| Current Path : /home/x/b/o/xbodynamge/namtation/wp-content/ |
| Current File : /home/x/b/o/xbodynamge/namtation/wp-content/Common.tar |
Integrations/WpCode.php 0000666 00000006041 15113050716 0011110 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Integrations;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Route class for the API.
*
* @since 4.3.8
*/
class WpCode {
/**
* Load the WPCode snippets for our desired username or return an empty array if not available.
*
* @since 4.3.8
*
* @return array The snippets.
*/
public static function loadWpCodeSnippets() {
$snippets = self::getPlaceholderSnippets();
if ( function_exists( 'wpcode_get_library_snippets_by_username' ) ) {
$snippets = wpcode_get_library_snippets_by_username( 'aioseo' );
}
return $snippets;
}
/**
* Checks if the plugin is installed, either the lite or premium version.
*
* @since 4.3.8
*
* @return bool True if the plugin is installed.
*/
public static function isPluginInstalled() {
return self::isProInstalled() || self::isLiteInstalled();
}
/**
* Is the pro plugin installed.
*
* @since 4.3.8
*
* @return bool True if the pro plugin is installed.
*/
public static function isProInstalled() {
$installedPlugins = array_keys( get_plugins() );
return in_array( 'wpcode-premium/wpcode.php', $installedPlugins, true );
}
/**
* Is the lite plugin installed.
*
* @since 4.3.8
*
* @return bool True if the lite plugin is installed.
*/
public static function isLiteInstalled() {
$installedPlugins = array_keys( get_plugins() );
return in_array( 'insert-headers-and-footers/ihaf.php', $installedPlugins, true );
}
/**
* Basic check if the plugin is active by looking for the main function.
*
* @since 4.3.8
*
* @return bool True if the plugin is active.
*/
public static function isPluginActive() {
return function_exists( 'wpcode' );
}
/**
* Checks if the plugin is active but needs to be updated by checking if the function to load the
* library snippets by username exists.
*
* @since 4.3.8
*
* @return bool True if the plugin is active but needs to be updated.
*/
public static function pluginNeedsUpdate() {
return self::isPluginActive() && ! function_exists( 'wpcode_get_library_snippets_by_username' );
}
/**
* Get placeholder snippets if the WPCode snippets are not available.
*
* @since 4.3.8
*
* @return array The placeholder snippets.
*/
private static function getPlaceholderSnippets() {
$snippetTitles = [
'Disable autogenerated shipping details schema for WooCommerce',
'Disable SEO Preview feature',
'Disable Shortcode Parsing in All in One SEO',
'Enable WooCommerce Product Attributes in Search Appearance',
'Fix LearnPress conflict that hides AIOSEO tabs on settings pages',
'Limit Meta Description to 160 characters',
'Limit SEO Title to 60 characters',
'Noindex Product Search Pages',
'Noindex Products under a Product Category',
];
$placeholderSnippets = [];
foreach ( $snippetTitles as $snippetTitle ) {
// Add placeholder install link so we show a button.
$placeholderSnippets[] = [
'title' => $snippetTitle,
'install' => 'https://library.wpcode.com/'
];
}
return $placeholderSnippets;
}
} Integrations/BbPress.php 0000666 00000000776 15113050716 0011300 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Integrations;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class to integrate with the bbPress plugin.
*
* @since 4.8.1
*/
class BbPress {
/**
* Returns whether the current page is a bbPress component page.
*
* @since 4.8.1
*
* @return bool Whether the current page is a bbPress component page.
*/
public static function isComponentPage() {
return ! empty( aioseo()->standalone->bbPress->component->templateType );
}
} Integrations/BuddyPress.php 0000666 00000010740 15113050716 0012014 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Integrations;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class to integrate with the BuddyPress plugin.
*
* @since 4.7.6
*/
class BuddyPress {
/**
* Call the callback given by the first parameter.
*
* @since 4.7.6
*
* @param callable $callback The function to be called.
* @param mixed ...$args Zero or more parameters to be passed to the function
* @return mixed|null The function result or null if the function is not callable.
*/
public static function callFunc( $callback, ...$args ) {
if ( is_callable( $callback ) ) {
return call_user_func( $callback, ...$args );
}
return null;
}
/**
* Returns the BuddyPress email custom post type slug.
*
* @since 4.7.6
*
* @return string The BuddyPress email custom post type slug if found or an empty string.
*/
public static function getEmailCptSlug() {
$slug = '';
if ( aioseo()->helpers->isPluginActive( 'buddypress' ) ) {
$slug = self::callFunc( 'bp_get_email_post_type' );
}
return is_scalar( $slug ) ? strval( $slug ) : '';
}
/**
* Retrieves the BuddyPress component archive page permalink.
*
* @since 4.7.6
*
* @param string $component The BuddyPress component.
* @return string The component archive page permalink.
*/
public static function getComponentArchiveUrl( $component ) {
switch ( $component ) {
case 'activity':
$output = self::callFunc( 'bp_get_activity_directory_permalink' );
break;
case 'member':
$output = self::callFunc( 'bp_get_members_directory_permalink' );
break;
case 'group':
$output = self::callFunc( 'bp_get_groups_directory_url' );
break;
default:
$output = '';
}
return is_scalar( $output ) ? strval( $output ) : '';
}
/**
* Returns the BuddyPress component single page permalink.
*
* @since 4.7.6
*
* @param string $component The BuddyPress component.
* @param mixed $id The component ID.
* @return string The component single page permalink.
*/
public static function getComponentSingleUrl( $component, $id ) {
switch ( $component ) {
case 'activity':
$output = self::callFunc( 'bp_activity_get_permalink', $id );
break;
case 'group':
$output = self::callFunc( 'bp_get_group_url', $id );
break;
case 'member':
$output = self::callFunc( 'bp_core_get_userlink', $id, false, true );
break;
default:
$output = '';
}
return is_scalar( $output ) ? strval( $output ) : '';
}
/**
* Returns the BuddyPress component edit link.
*
* @since 4.7.6
*
* @param string $component The BuddyPress component.
* @param mixed $id The component ID.
* @return string The component edit link.
*/
public static function getComponentEditUrl( $component, $id ) {
switch ( $component ) {
case 'activity':
$output = add_query_arg( [
'page' => 'bp-activity',
'aid' => $id,
'action' => 'edit'
], self::callFunc( 'bp_get_admin_url', 'admin.php' ) );
break;
case 'group':
$output = add_query_arg( [
'page' => 'bp-groups',
'gid' => $id,
'action' => 'edit'
], self::callFunc( 'bp_get_admin_url', 'admin.php' ) );
break;
case 'member':
$output = get_edit_user_link( $id );
break;
default:
$output = '';
}
return is_scalar( $output ) ? strval( $output ) : '';
}
/**
* Returns whether the BuddyPress component is active or not.
*
* @since 4.7.6
*
* @param string $component The BuddyPress component.
* @return bool Whether the BuddyPress component is active.
*/
public static function isComponentActive( $component ) {
static $active = [];
if ( isset( $active[ $component ] ) ) {
return $active[ $component ];
}
switch ( $component ) {
case 'activity':
$active[ $component ] = self::callFunc( 'bp_is_active', 'activity' );
break;
case 'group':
$active[ $component ] = self::callFunc( 'bp_is_active', 'groups' );
break;
case 'member':
$active[ $component ] = self::callFunc( 'bp_is_active', 'members' );
break;
default:
$active[ $component ] = false;
}
return $active[ $component ];
}
/**
* Returns whether the current page is a BuddyPress component page.
*
* @since 4.7.6
*
* @return bool Whether the current page is a BuddyPress component page.
*/
public static function isComponentPage() {
return ! empty( aioseo()->standalone->buddyPress->component->templateType );
}
} Integrations/Semrush.php 0000666 00000013171 15113050716 0011357 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Integrations;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class to integrate with the Semrush API.
*
* @since 4.0.16
*/
class Semrush {
/**
* The Oauth2 URL.
*
* @since 4.0.16
*
* @var string
*/
public static $url = 'https://oauth.semrush.com/oauth2/access_token';
/**
* The client ID for the Oauth2 integration.
*
* @since 4.0.16
*
* @var string
*/
public static $clientId = 'aioseo';
/**
* The client secret for the Oauth2 integration.
*
* @since 4.0.16
*
* @var string
*/
public static $clientSecret = 'sdDUjYt6umO7sKM7mp4OrN8yeePTOQBy';
/**
* Static method to authenticate the user.
*
* @since 4.0.16
*
* @param string $authorizationCode The authorization code for the Oauth2 authentication.
* @return bool Whether the user is succesfully authenticated.
*/
public static function authenticate( $authorizationCode ) {
$time = time();
$response = wp_remote_post( self::$url, [
'headers' => [ 'Content-Type' => 'application/json' ],
'body' => wp_json_encode( [
'client_id' => self::$clientId,
'client_secret' => self::$clientSecret,
'grant_type' => 'authorization_code',
'code' => $authorizationCode,
'redirect_uri' => 'https://oauth.semrush.com/oauth2/aioseo/success'
] )
] );
$responseCode = wp_remote_retrieve_response_code( $response );
if ( 200 === $responseCode ) {
$tokens = json_decode( wp_remote_retrieve_body( $response ) );
return self::saveTokens( $tokens, $time );
}
return false;
}
/**
* Static method to refresh the tokens once expired.
*
* @since 4.0.16
*
* @return bool Whether the tokens were successfully renewed.
*/
public static function refreshTokens() {
$refreshToken = aioseo()->internalOptions->integrations->semrush->refreshToken;
if ( empty( $refreshToken ) ) {
self::reset();
return false;
}
$time = time();
$response = wp_remote_post( self::$url, [
'headers' => [ 'Content-Type' => 'application/json' ],
'body' => wp_json_encode( [
'client_id' => self::$clientId,
'client_secret' => self::$clientSecret,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
] )
] );
$responseCode = wp_remote_retrieve_response_code( $response );
if ( 200 === $responseCode ) {
$tokens = json_decode( wp_remote_retrieve_body( $response ) );
return self::saveTokens( $tokens, $time );
}
return false;
}
/**
* Clears out the internal options to reset the tokens.
*
* @since 4.1.5
*
* @return void
*/
private static function reset() {
aioseo()->internalOptions->integrations->semrush->accessToken = '';
aioseo()->internalOptions->integrations->semrush->tokenType = '';
aioseo()->internalOptions->integrations->semrush->expires = '';
aioseo()->internalOptions->integrations->semrush->refreshToken = '';
}
/**
* Checks if the token has expired
*
* @since 4.0.16
*
* @return boolean Whether or not the token has expired.
*/
public static function hasExpired() {
$tokens = self::getTokens();
return time() >= $tokens['expires'];
}
/**
* Returns the tokens.
*
* @since 4.0.16
*
* @return array An array of token data.
*/
public static function getTokens() {
return aioseo()->internalOptions->integrations->semrush->all();
}
/**
* Saves the token options.
*
* @since 4.0.16
*
* @param Object $tokens The tokens object.
* @param string $time The time set before the request was made.
* @return bool Whether the response was valid and successfully saved.
*/
public static function saveTokens( $tokens, $time ) {
$expectedProps = [
'access_token',
'token_type',
'expires_in',
'refresh_token'
];
// If the oAuth response does not include all expected properties, drop it.
foreach ( $expectedProps as $prop ) {
if ( empty( $tokens->$prop ) ) {
return false;
}
}
// Save the options.
aioseo()->internalOptions->integrations->semrush->accessToken = $tokens->access_token;
aioseo()->internalOptions->integrations->semrush->tokenType = $tokens->token_type;
aioseo()->internalOptions->integrations->semrush->expires = $time + $tokens->expires_in;
aioseo()->internalOptions->integrations->semrush->refreshToken = $tokens->refresh_token;
return true;
}
/**
* API call to get keyphrases from semrush.
*
* @since 4.0.16
*
* @param string $keyphrase A primary keyphrase.
* @param string $database A country database.
* @return object|bool The response object or false if the tokens could not be refreshed.
*/
public static function getKeyphrases( $keyphrase, $database ) {
if ( self::hasExpired() ) {
$success = self::refreshTokens();
if ( ! $success ) {
return false;
}
}
$transientKey = 'semrush_keyphrases_' . $keyphrase . '_' . $database;
$results = aioseo()->core->cache->get( $transientKey );
if ( null !== $results ) {
return $results;
}
$params = [
'phrase' => $keyphrase,
'export_columns' => 'Ph,Nq,Td',
'database' => strtolower( $database ),
'display_limit' => 10,
'display_offset' => 0,
'display_sort' => 'nq_desc',
'display_filter' => '%2B|Nq|Lt|1000',
'access_token' => aioseo()->internalOptions->integrations->semrush->accessToken
];
$url = 'https://oauth.semrush.com/api/v1/keywords/phrase_fullsearch?' . http_build_query( $params );
$response = wp_remote_get( $url );
$body = json_decode( wp_remote_retrieve_body( $response ) );
aioseo()->core->cache->update( $transientKey, $body );
return $body;
}
} ThirdParty/ThirdParty.php 0000666 00000000672 15113050716 0011451 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ThirdParty;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Instantiates our third-party classes.
*
* @since 4.7.6
*/
class ThirdParty {
/**
* WebStories instance.
*
* @since 4.7.6
*
* @var WebStories
*/
public $webStories;
/**
* Class constructor.
*
* @since 4.7.6
*/
public function __construct() {
$this->webStories = new WebStories();
}
} ThirdParty/WebStories.php 0000666 00000002767 15113050716 0011454 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ThirdParty;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Integrates with Google Web Stories plugin.
*
* @since 4.8.3
*/
class WebStories {
/**
* Class constructor.
*
* @since 4.7.6
*/
public function __construct() {
add_action( 'web_stories_story_head', [ $this, 'stripDefaultTags' ], 0 );
add_action( 'web_stories_story_head', [ $this, 'outputAioseoTags' ] );
}
/**
* Strip all meta tags that are added by default by the Web Stories plugin.
*
* @since 4.7.6
*
* @return void
*/
public function stripDefaultTags() {
add_filter( 'web_stories_enable_metadata', '__return_false' );
add_filter( 'web_stories_enable_schemaorg_metadata', '__return_false' );
add_filter( 'web_stories_enable_open_graph_metadata', '__return_false' );
add_filter( 'web_stories_enable_twitter_metadata', '__return_false' );
remove_action( 'web_stories_story_head', 'rel_canonical' );
remove_action( 'web_stories_story_head', 'wp_robots' );
// This is needed to prevent multiple robots meta tags from being output.
add_filter( 'wp_robots', '__return_empty_array' );
}
/**
* Output the AIOSEO tags.
*
* @since 4.7.6
*
* @return void
*/
public function outputAioseoTags() {
aioseo()->head->wpHead();
}
/**
* Checks if the plugin is active.
*
* @since 4.7.6
*
* @return bool True if the plugin is active.
*/
public function isPluginActive() {
return class_exists( 'Google\Web_Stories\Plugin' );
}
} WritingAssistant/Utils/Helpers.php 0000666 00000043000 15113050716 0013274 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\WritingAssistant\Utils;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Helper functions.
*
* @since 4.7.4
*/
class Helpers {
/**
* Gets the data for vue.
*
* @since 4.7.4
*
* @return array An array of data.
*/
public function getStandaloneVueData() {
$keyword = Models\WritingAssistantPost::getKeyword( get_the_ID() );
return [
'postId' => get_the_ID(),
'report' => $keyword,
'keywordText' => ! empty( $keyword->keyword ) ? $keyword->keyword : '',
'contentAnalysis' => Models\WritingAssistantPost::getContentAnalysis( get_the_ID() ),
'seoBoost' => [
'isLoggedIn' => aioseo()->writingAssistant->seoBoost->isLoggedIn(),
'loginUrl' => aioseo()->writingAssistant->seoBoost->getLoginUrl(),
'createAccountUrl' => aioseo()->writingAssistant->seoBoost->getCreateAccountUrl(),
'userOptions' => aioseo()->writingAssistant->seoBoost->getUserOptions()
]
];
}
/**
* Gets the data for vue.
*
* @since 4.7.4
*
* @return array An array of data.
*/
public function getSettingsVueData() {
return [
'seoBoost' => [
'isLoggedIn' => aioseo()->writingAssistant->seoBoost->isLoggedIn(),
'loginUrl' => aioseo()->writingAssistant->seoBoost->getLoginUrl(),
'createAccountUrl' => aioseo()->writingAssistant->seoBoost->getCreateAccountUrl(),
'userOptions' => aioseo()->writingAssistant->seoBoost->getUserOptions(),
'countries' => $this->getCountries(),
'languages' => $this->getLanguages(),
'searchEngines' => $this->getSearchEngines()
]
];
}
/**
* Returns the list of countries.
*
* @since 4.7.7.1
* @version 4.8.3 Moved from SeoBoost/SeoBoost.php
*
* @return array The list of countries.
*/
private function getCountries() {
$countries = [
'AF' => __( 'Afghanistan', 'all-in-one-seo-pack' ),
'AL' => __( 'Albania', 'all-in-one-seo-pack' ),
'DZ' => __( 'Algeria', 'all-in-one-seo-pack' ),
'AS' => __( 'American Samoa', 'all-in-one-seo-pack' ),
'AD' => __( 'Andorra', 'all-in-one-seo-pack' ),
'AO' => __( 'Angola', 'all-in-one-seo-pack' ),
'AI' => __( 'Anguilla', 'all-in-one-seo-pack' ),
'AG' => __( 'Antigua & Barbuda', 'all-in-one-seo-pack' ),
'AR' => __( 'Argentina', 'all-in-one-seo-pack' ),
'AM' => __( 'Armenia', 'all-in-one-seo-pack' ),
'AU' => __( 'Australia', 'all-in-one-seo-pack' ),
'AT' => __( 'Austria', 'all-in-one-seo-pack' ),
'AZ' => __( 'Azerbaijan', 'all-in-one-seo-pack' ),
'BS' => __( 'Bahamas', 'all-in-one-seo-pack' ),
'BH' => __( 'Bahrain', 'all-in-one-seo-pack' ),
'BD' => __( 'Bangladesh', 'all-in-one-seo-pack' ),
'BY' => __( 'Belarus', 'all-in-one-seo-pack' ),
'BE' => __( 'Belgium', 'all-in-one-seo-pack' ),
'BZ' => __( 'Belize', 'all-in-one-seo-pack' ),
'BJ' => __( 'Benin', 'all-in-one-seo-pack' ),
'BT' => __( 'Bhutan', 'all-in-one-seo-pack' ),
'BO' => __( 'Bolivia', 'all-in-one-seo-pack' ),
'BA' => __( 'Bosnia & Herzegovina', 'all-in-one-seo-pack' ),
'BW' => __( 'Botswana', 'all-in-one-seo-pack' ),
'BR' => __( 'Brazil', 'all-in-one-seo-pack' ),
'VG' => __( 'British Virgin Islands', 'all-in-one-seo-pack' ),
'BN' => __( 'Brunei', 'all-in-one-seo-pack' ),
'BG' => __( 'Bulgaria', 'all-in-one-seo-pack' ),
'BF' => __( 'Burkina Faso', 'all-in-one-seo-pack' ),
'BI' => __( 'Burundi', 'all-in-one-seo-pack' ),
'KH' => __( 'Cambodia', 'all-in-one-seo-pack' ),
'CM' => __( 'Cameroon', 'all-in-one-seo-pack' ),
'CA' => __( 'Canada', 'all-in-one-seo-pack' ),
'CV' => __( 'Cape Verde', 'all-in-one-seo-pack' ),
'CF' => __( 'Central African Republic', 'all-in-one-seo-pack' ),
'TD' => __( 'Chad', 'all-in-one-seo-pack' ),
'CL' => __( 'Chile', 'all-in-one-seo-pack' ),
'CO' => __( 'Colombia', 'all-in-one-seo-pack' ),
'CG' => __( 'Congo - Brazzaville', 'all-in-one-seo-pack' ),
'CD' => __( 'Congo - Kinshasa', 'all-in-one-seo-pack' ),
'CK' => __( 'Cook Islands', 'all-in-one-seo-pack' ),
'CR' => __( 'Costa Rica', 'all-in-one-seo-pack' ),
'CI' => __( 'Côte d’Ivoire', 'all-in-one-seo-pack' ),
'HR' => __( 'Croatia', 'all-in-one-seo-pack' ),
'CU' => __( 'Cuba', 'all-in-one-seo-pack' ),
'CY' => __( 'Cyprus', 'all-in-one-seo-pack' ),
'CZ' => __( 'Czechia', 'all-in-one-seo-pack' ),
'DK' => __( 'Denmark', 'all-in-one-seo-pack' ),
'DJ' => __( 'Djibouti', 'all-in-one-seo-pack' ),
'DM' => __( 'Dominica', 'all-in-one-seo-pack' ),
'DO' => __( 'Dominican Republic', 'all-in-one-seo-pack' ),
'EC' => __( 'Ecuador', 'all-in-one-seo-pack' ),
'EG' => __( 'Egypt', 'all-in-one-seo-pack' ),
'SV' => __( 'El Salvador', 'all-in-one-seo-pack' ),
'EE' => __( 'Estonia', 'all-in-one-seo-pack' ),
'ET' => __( 'Ethiopia', 'all-in-one-seo-pack' ),
'FJ' => __( 'Fiji', 'all-in-one-seo-pack' ),
'FI' => __( 'Finland', 'all-in-one-seo-pack' ),
'FR' => __( 'France', 'all-in-one-seo-pack' ),
'GA' => __( 'Gabon', 'all-in-one-seo-pack' ),
'GM' => __( 'Gambia', 'all-in-one-seo-pack' ),
'GE' => __( 'Georgia', 'all-in-one-seo-pack' ),
'DE' => __( 'Germany', 'all-in-one-seo-pack' ),
'GH' => __( 'Ghana', 'all-in-one-seo-pack' ),
'GI' => __( 'Gibraltar', 'all-in-one-seo-pack' ),
'GR' => __( 'Greece', 'all-in-one-seo-pack' ),
'GL' => __( 'Greenland', 'all-in-one-seo-pack' ),
'GT' => __( 'Guatemala', 'all-in-one-seo-pack' ),
'GG' => __( 'Guernsey', 'all-in-one-seo-pack' ),
'GY' => __( 'Guyana', 'all-in-one-seo-pack' ),
'HT' => __( 'Haiti', 'all-in-one-seo-pack' ),
'HN' => __( 'Honduras', 'all-in-one-seo-pack' ),
'HK' => __( 'Hong Kong', 'all-in-one-seo-pack' ),
'HU' => __( 'Hungary', 'all-in-one-seo-pack' ),
'IS' => __( 'Iceland', 'all-in-one-seo-pack' ),
'IN' => __( 'India', 'all-in-one-seo-pack' ),
'ID' => __( 'Indonesia', 'all-in-one-seo-pack' ),
'IQ' => __( 'Iraq', 'all-in-one-seo-pack' ),
'IE' => __( 'Ireland', 'all-in-one-seo-pack' ),
'IM' => __( 'Isle of Man', 'all-in-one-seo-pack' ),
'IL' => __( 'Israel', 'all-in-one-seo-pack' ),
'IT' => __( 'Italy', 'all-in-one-seo-pack' ),
'JM' => __( 'Jamaica', 'all-in-one-seo-pack' ),
'JP' => __( 'Japan', 'all-in-one-seo-pack' ),
'JE' => __( 'Jersey', 'all-in-one-seo-pack' ),
'JO' => __( 'Jordan', 'all-in-one-seo-pack' ),
'KZ' => __( 'Kazakhstan', 'all-in-one-seo-pack' ),
'KE' => __( 'Kenya', 'all-in-one-seo-pack' ),
'KI' => __( 'Kiribati', 'all-in-one-seo-pack' ),
'KW' => __( 'Kuwait', 'all-in-one-seo-pack' ),
'KG' => __( 'Kyrgyzstan', 'all-in-one-seo-pack' ),
'LA' => __( 'Laos', 'all-in-one-seo-pack' ),
'LV' => __( 'Latvia', 'all-in-one-seo-pack' ),
'LB' => __( 'Lebanon', 'all-in-one-seo-pack' ),
'LS' => __( 'Lesotho', 'all-in-one-seo-pack' ),
'LY' => __( 'Libya', 'all-in-one-seo-pack' ),
'LI' => __( 'Liechtenstein', 'all-in-one-seo-pack' ),
'LT' => __( 'Lithuania', 'all-in-one-seo-pack' ),
'LU' => __( 'Luxembourg', 'all-in-one-seo-pack' ),
'MG' => __( 'Madagascar', 'all-in-one-seo-pack' ),
'MW' => __( 'Malawi', 'all-in-one-seo-pack' ),
'MY' => __( 'Malaysia', 'all-in-one-seo-pack' ),
'MV' => __( 'Maldives', 'all-in-one-seo-pack' ),
'ML' => __( 'Mali', 'all-in-one-seo-pack' ),
'MT' => __( 'Malta', 'all-in-one-seo-pack' ),
'MU' => __( 'Mauritius', 'all-in-one-seo-pack' ),
'MX' => __( 'Mexico', 'all-in-one-seo-pack' ),
'FM' => __( 'Micronesia', 'all-in-one-seo-pack' ),
'MD' => __( 'Moldova', 'all-in-one-seo-pack' ),
'MN' => __( 'Mongolia', 'all-in-one-seo-pack' ),
'ME' => __( 'Montenegro', 'all-in-one-seo-pack' ),
'MS' => __( 'Montserrat', 'all-in-one-seo-pack' ),
'MA' => __( 'Morocco', 'all-in-one-seo-pack' ),
'MZ' => __( 'Mozambique', 'all-in-one-seo-pack' ),
'MM' => __( 'Myanmar (Burma)', 'all-in-one-seo-pack' ),
'NA' => __( 'Namibia', 'all-in-one-seo-pack' ),
'NR' => __( 'Nauru', 'all-in-one-seo-pack' ),
'NP' => __( 'Nepal', 'all-in-one-seo-pack' ),
'NL' => __( 'Netherlands', 'all-in-one-seo-pack' ),
'NZ' => __( 'New Zealand', 'all-in-one-seo-pack' ),
'NI' => __( 'Nicaragua', 'all-in-one-seo-pack' ),
'NE' => __( 'Niger', 'all-in-one-seo-pack' ),
'NG' => __( 'Nigeria', 'all-in-one-seo-pack' ),
'NU' => __( 'Niue', 'all-in-one-seo-pack' ),
'MK' => __( 'North Macedonia', 'all-in-one-seo-pack' ),
'NO' => __( 'Norway', 'all-in-one-seo-pack' ),
'OM' => __( 'Oman', 'all-in-one-seo-pack' ),
'PK' => __( 'Pakistan', 'all-in-one-seo-pack' ),
'PS' => __( 'Palestine', 'all-in-one-seo-pack' ),
'PA' => __( 'Panama', 'all-in-one-seo-pack' ),
'PG' => __( 'Papua New Guinea', 'all-in-one-seo-pack' ),
'PY' => __( 'Paraguay', 'all-in-one-seo-pack' ),
'PE' => __( 'Peru', 'all-in-one-seo-pack' ),
'PH' => __( 'Philippines', 'all-in-one-seo-pack' ),
'PN' => __( 'Pitcairn Islands', 'all-in-one-seo-pack' ),
'PL' => __( 'Poland', 'all-in-one-seo-pack' ),
'PT' => __( 'Portugal', 'all-in-one-seo-pack' ),
'PR' => __( 'Puerto Rico', 'all-in-one-seo-pack' ),
'QA' => __( 'Qatar', 'all-in-one-seo-pack' ),
'RO' => __( 'Romania', 'all-in-one-seo-pack' ),
'RU' => __( 'Russia', 'all-in-one-seo-pack' ),
'RW' => __( 'Rwanda', 'all-in-one-seo-pack' ),
'WS' => __( 'Samoa', 'all-in-one-seo-pack' ),
'SM' => __( 'San Marino', 'all-in-one-seo-pack' ),
'ST' => __( 'São Tomé & Príncipe', 'all-in-one-seo-pack' ),
'SA' => __( 'Saudi Arabia', 'all-in-one-seo-pack' ),
'SN' => __( 'Senegal', 'all-in-one-seo-pack' ),
'RS' => __( 'Serbia', 'all-in-one-seo-pack' ),
'SC' => __( 'Seychelles', 'all-in-one-seo-pack' ),
'SL' => __( 'Sierra Leone', 'all-in-one-seo-pack' ),
'SG' => __( 'Singapore', 'all-in-one-seo-pack' ),
'SK' => __( 'Slovakia', 'all-in-one-seo-pack' ),
'SI' => __( 'Slovenia', 'all-in-one-seo-pack' ),
'SB' => __( 'Solomon Islands', 'all-in-one-seo-pack' ),
'SO' => __( 'Somalia', 'all-in-one-seo-pack' ),
'ZA' => __( 'South Africa', 'all-in-one-seo-pack' ),
'KR' => __( 'South Korea', 'all-in-one-seo-pack' ),
'ES' => __( 'Spain', 'all-in-one-seo-pack' ),
'LK' => __( 'Sri Lanka', 'all-in-one-seo-pack' ),
'SH' => __( 'St. Helena', 'all-in-one-seo-pack' ),
'VC' => __( 'St. Vincent & Grenadines', 'all-in-one-seo-pack' ),
'SR' => __( 'Suriname', 'all-in-one-seo-pack' ),
'SE' => __( 'Sweden', 'all-in-one-seo-pack' ),
'CH' => __( 'Switzerland', 'all-in-one-seo-pack' ),
'TW' => __( 'Taiwan', 'all-in-one-seo-pack' ),
'TJ' => __( 'Tajikistan', 'all-in-one-seo-pack' ),
'TZ' => __( 'Tanzania', 'all-in-one-seo-pack' ),
'TH' => __( 'Thailand', 'all-in-one-seo-pack' ),
'TL' => __( 'Timor-Leste', 'all-in-one-seo-pack' ),
'TG' => __( 'Togo', 'all-in-one-seo-pack' ),
'TO' => __( 'Tonga', 'all-in-one-seo-pack' ),
'TT' => __( 'Trinidad & Tobago', 'all-in-one-seo-pack' ),
'TN' => __( 'Tunisia', 'all-in-one-seo-pack' ),
'TR' => __( 'Turkey', 'all-in-one-seo-pack' ),
'TM' => __( 'Turkmenistan', 'all-in-one-seo-pack' ),
'VI' => __( 'U.S. Virgin Islands', 'all-in-one-seo-pack' ),
'UG' => __( 'Uganda', 'all-in-one-seo-pack' ),
'UA' => __( 'Ukraine', 'all-in-one-seo-pack' ),
'AE' => __( 'United Arab Emirates', 'all-in-one-seo-pack' ),
'GB' => __( 'United Kingdom', 'all-in-one-seo-pack' ),
'US' => __( 'United States', 'all-in-one-seo-pack' ),
'UY' => __( 'Uruguay', 'all-in-one-seo-pack' ),
'UZ' => __( 'Uzbekistan', 'all-in-one-seo-pack' ),
'VU' => __( 'Vanuatu', 'all-in-one-seo-pack' ),
'VE' => __( 'Venezuela', 'all-in-one-seo-pack' ),
'VN' => __( 'Vietnam', 'all-in-one-seo-pack' ),
'ZM' => __( 'Zambia', 'all-in-one-seo-pack' ),
'ZW' => __( 'Zimbabwe', 'all-in-one-seo-pack' )
];
return $countries;
}
/**
* Returns the list of languages.
*
* @since 4.7.7.1
* @version 4.8.3 Moved from SeoBoost/SeoBoost.php
*
* @return array The list of languages.
*/
private function getLanguages() {
$languages = [
'ca' => __( 'Catalan', 'all-in-one-seo-pack' ),
'da' => __( 'Danish', 'all-in-one-seo-pack' ),
'nl' => __( 'Dutch', 'all-in-one-seo-pack' ),
'en' => __( 'English', 'all-in-one-seo-pack' ),
'fr' => __( 'French', 'all-in-one-seo-pack' ),
'de' => __( 'German', 'all-in-one-seo-pack' ),
'id' => __( 'Indonesian', 'all-in-one-seo-pack' ),
'it' => __( 'Italian', 'all-in-one-seo-pack' ),
'no' => __( 'Norwegian', 'all-in-one-seo-pack' ),
'pt' => __( 'Portuguese', 'all-in-one-seo-pack' ),
'ro' => __( 'Romanian', 'all-in-one-seo-pack' ),
'es' => __( 'Spanish', 'all-in-one-seo-pack' ),
'sv' => __( 'Swedish', 'all-in-one-seo-pack' ),
'tr' => __( 'Turkish', 'all-in-one-seo-pack' )
];
return $languages;
}
/**
* Returns the list of search engines.
*
* @since 4.7.7.1
* @version 4.8.3 Moved from SeoBoost/SeoBoost.php
*
* @return array The list of search engines.
*/
private function getSearchEngines() {
$searchEngines = [
'AF' => 'google.com.af',
'AL' => 'google.al',
'DZ' => 'google.dz',
'AS' => 'google.as',
'AD' => 'google.ad',
'AO' => 'google.it.ao',
'AI' => 'google.com.ai',
'AG' => 'google.com.ag',
'AR' => 'google.com.ar',
'AM' => 'google.am',
'AU' => 'google.com.au',
'AT' => 'google.at',
'AZ' => 'google.az',
'BS' => 'google.bs',
'BH' => 'google.com.bh',
'BD' => 'google.com.bd',
'BY' => 'google.com.by',
'BE' => 'google.be',
'BZ' => 'google.com.bz',
'BJ' => 'google.bj',
'BT' => 'google.bt',
'BO' => 'google.com.bo',
'BA' => 'google.ba',
'BW' => 'google.co.bw',
'BR' => 'google.com.br',
'VG' => 'google.vg',
'BN' => 'google.com.bn',
'BG' => 'google.bg',
'BF' => 'google.bf',
'BI' => 'google.bi',
'KH' => 'google.com.kh',
'CM' => 'google.cm',
'CA' => 'google.ca',
'CV' => 'google.cv',
'CF' => 'google.cf',
'TD' => 'google.td',
'CL' => 'google.cl',
'CO' => 'google.com.co',
'CG' => 'google.cg',
'CD' => 'google.cd',
'CK' => 'google.co.ck',
'CR' => 'google.co.cr',
'CI' => 'google.ci',
'HR' => 'google.hr',
'CU' => 'google.com.cu',
'CY' => 'google.com.cy',
'CZ' => 'google.cz',
'DK' => 'google.dk',
'DJ' => 'google.dj',
'DM' => 'google.dm',
'DO' => 'google.com.do',
'EC' => 'google.com.ec',
'EG' => 'google.com.eg',
'SV' => 'google.com.sv',
'EE' => 'google.ee',
'ET' => 'google.com.et',
'FJ' => 'google.com.fj',
'FI' => 'google.fi',
'FR' => 'google.fr',
'GA' => 'google.ga',
'GM' => 'google.gm',
'GE' => 'google.ge',
'DE' => 'google.de',
'GH' => 'google.com.gh',
'GI' => 'google.com.gi',
'GR' => 'google.gr',
'GL' => 'google.gl',
'GT' => 'google.com.gt',
'GG' => 'google.gg',
'GY' => 'google.gy',
'HT' => 'google.ht',
'HN' => 'google.hn',
'HK' => 'google.com.hk',
'HU' => 'google.hu',
'IS' => 'google.is',
'IN' => 'google.co.in',
'ID' => 'google.co.id',
'IQ' => 'google.iq',
'IE' => 'google.ie',
'IM' => 'google.co.im',
'IL' => 'google.co.il',
'IT' => 'google.it',
'JM' => 'google.com.jm',
'JP' => 'google.co.jp',
'JE' => 'google.co.je',
'JO' => 'google.jo',
'KZ' => 'google.kz',
'KE' => 'google.co.ke',
'KI' => 'google.ki',
'KW' => 'google.com.kw',
'KG' => 'google.com.kg',
'LA' => 'google.la',
'LV' => 'google.lv',
'LB' => 'google.com.lb',
'LS' => 'google.co.ls',
'LY' => 'google.com.ly',
'LI' => 'google.li',
'LT' => 'google.lt',
'LU' => 'google.lu',
'MG' => 'google.mg',
'MW' => 'google.mw',
'MY' => 'google.com.my',
'MV' => 'google.mv',
'ML' => 'google.ml',
'MT' => 'google.com.mt',
'MU' => 'google.mu',
'MX' => 'google.com.mx',
'FM' => 'google.fm',
'MD' => 'google.md',
'MN' => 'google.mn',
'ME' => 'google.me',
'MS' => 'google.ms',
'MA' => 'google.co.ma',
'MZ' => 'google.co.mz',
'MM' => 'google.com.mm',
'NA' => 'google.com.na',
'NR' => 'google.nr',
'NP' => 'google.com.np',
'NL' => 'google.nl',
'NZ' => 'google.co.nz',
'NI' => 'google.com.ni',
'NE' => 'google.ne',
'NG' => 'google.com.ng',
'NU' => 'google.nu',
'MK' => 'google.mk',
'NO' => 'google.no',
'OM' => 'google.com.om',
'PK' => 'google.com.pk',
'PS' => 'google.ps',
'PA' => 'google.com.pa',
'PG' => 'google.com.pg',
'PY' => 'google.com.py',
'PE' => 'google.com.pe',
'PH' => 'google.com.ph',
'PN' => 'google.pn',
'PL' => 'google.pl',
'PT' => 'google.pt',
'PR' => 'google.com.pr',
'QA' => 'google.com.qa',
'RO' => 'google.ro',
'RU' => 'google.ru',
'RW' => 'google.rw',
'WS' => 'google.as',
'SM' => 'google.sm',
'ST' => 'google.st',
'SA' => 'google.com.sa',
'SN' => 'google.sn',
'RS' => 'google.rs',
'SC' => 'google.sc',
'SL' => 'google.com.sl',
'SG' => 'google.com.sg',
'SK' => 'google.sk',
'SI' => 'google.si',
'SB' => 'google.com.sb',
'SO' => 'google.so',
'ZA' => 'google.co.za',
'KR' => 'google.co.kr',
'ES' => 'google.es',
'LK' => 'google.lk',
'SH' => 'google.sh',
'VC' => 'google.com.vc',
'SR' => 'google.sr',
'SE' => 'google.se',
'CH' => 'google.ch',
'TW' => 'google.com.tw',
'TJ' => 'google.com.tj',
'TZ' => 'google.co.tz',
'TH' => 'google.co.th',
'TL' => 'google.tl',
'TG' => 'google.tg',
'TO' => 'google.to',
'TT' => 'google.tt',
'TN' => 'google.tn',
'TR' => 'google.com.tr',
'TM' => 'google.tm',
'VI' => 'google.co.vi',
'UG' => 'google.co.ug',
'UA' => 'google.com.ua',
'AE' => 'google.ae',
'GB' => 'google.co.uk',
'US' => 'google.com',
'UY' => 'google.com.uy',
'UZ' => 'google.co.uz',
'VU' => 'google.vu',
'VE' => 'google.co.ve',
'VN' => 'google.com.vn',
'ZM' => 'google.co.zm',
'ZW' => 'google.co.zw'
];
return $searchEngines;
}
}WritingAssistant/SeoBoost/SeoBoost.php 0000666 00000020236 15113050716 0014072 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\WritingAssistant\SeoBoost;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the connection with SEOBoost.
*
* @since 4.7.4
*/
class SeoBoost {
/**
* URL of the login page.
*
* @since 4.7.4
*/
private $loginUrl = 'https://app.seoboost.com/login/';
/**
* URL of the Create Account page.
*
* @since 4.7.4
*/
private $createAccountUrl = 'https://seoboost.com/checkout/';
/**
* The service.
*
* @since 4.7.4
*
* @var Service
*/
public $service;
/**
* Class constructor.
*
* @since 4.7.4
*/
public function __construct() {
$this->service = new Service();
$returnParam = isset( $_GET['aioseo-writing-assistant'] ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
? sanitize_text_field( wp_unslash( $_GET['aioseo-writing-assistant'] ) ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
: null;
if ( 'auth_return' === $returnParam ) {
add_action( 'init', [ $this, 'checkToken' ], 50 );
}
if ( 'ms_logged_in' === $returnParam ) {
add_action( 'init', [ $this, 'marketingSiteCallback' ], 50 );
}
add_action( 'init', [ $this, 'migrateUserData' ], 10 );
add_action( 'init', [ $this, 'refreshUserOptionsAfterError' ] );
}
/**
* Returns if the user has an access key.
*
* @since 4.7.4
*
* @return bool
*/
public function isLoggedIn() {
return $this->getAccessToken() !== '';
}
/**
* Gets the login URL.
*
* @since 4.7.4
*
* @return string The login URL.
*/
public function getLoginUrl() {
$url = $this->loginUrl;
if ( defined( 'AIOSEO_WRITING_ASSISTANT_LOGIN_URL' ) ) {
$url = AIOSEO_WRITING_ASSISTANT_LOGIN_URL;
}
$params = [
'oauth' => true,
'redirect' => get_site_url() . '?' . build_query( [ 'aioseo-writing-assistant' => 'auth_return' ] ),
'domain' => aioseo()->helpers->getMultiSiteDomain()
];
return trailingslashit( $url ) . '?' . build_query( $params );
}
/**
* Gets the login URL.
*
* @since 4.7.4
*
* @return string The login URL.
*/
public function getCreateAccountUrl() {
$url = $this->createAccountUrl;
if ( defined( 'AIOSEO_WRITING_ASSISTANT_CREATE_ACCOUNT_URL' ) ) {
$url = AIOSEO_WRITING_ASSISTANT_CREATE_ACCOUNT_URL;
}
$params = [
'url' => base64_encode( get_site_url() . '?' . build_query( [ 'aioseo-writing-assistant' => 'ms_logged_in' ] ) ),
'writing-assistant-checkout' => true
];
return trailingslashit( $url ) . '?' . build_query( $params );
}
/**
* Gets the user's access token.
*
* @since 4.7.4
*
* @return string The access token.
*/
public function getAccessToken() {
$metaKey = 'seoboost_access_token_' . get_current_blog_id();
return get_user_meta( get_current_user_id(), $metaKey, true );
}
/**
* Sets the user's access token.
*
* @since 4.7.4
*
* @return void
*/
public function setAccessToken( $accessToken ) {
$metaKey = 'seoboost_access_token_' . get_current_blog_id();
update_user_meta( get_current_user_id(), $metaKey, $accessToken );
$this->refreshUserOptions();
}
/**
* Refreshes user options from SEOBoost.
*
* @since 4.7.4
*
* @return void
*/
public function refreshUserOptions() {
$userOptions = $this->service->getUserOptions();
if ( is_wp_error( $userOptions ) || ! empty( $userOptions['error'] ) ) {
$userOptions = $this->getDefaultUserOptions();
aioseo()->cache->update( 'seoboost_get_user_options_error', time() + DAY_IN_SECONDS, MONTH_IN_SECONDS );
}
$this->setUserOptions( $userOptions );
}
/**
* Gets the user options.
*
* @since 4.7.4
*
* @param bool $refresh Whether to refresh the user options.
* @return array The user options.
*/
public function getUserOptions( $refresh = false ) {
if ( ! $refresh ) {
$metaKey = 'seoboost_user_options_' . get_current_blog_id();
$userOptions = get_user_meta( get_current_user_id(), $metaKey, true );
if ( ! empty( $userOptions ) ) {
return json_decode( (string) $userOptions, true ) ?? [];
}
}
// If there are no options or we need to refresh them, get them from SEOBoost.
$this->refreshUserOptions();
$userOptions = $this->getUserOptions();
if ( empty( $userOptions ) ) {
return $this->getDefaultUserOptions();
}
return $userOptions;
}
/**
* Gets the user options.
*
* @since 4.7.4
*
* @param array $options The user options.
* @return void
*/
public function setUserOptions( $options ) {
if ( ! is_array( $options ) ) {
return;
}
$metaKey = 'seoboost_user_options_' . get_current_blog_id();
$userOptions = array_intersect_key( $options, $this->getDefaultUserOptions() );
update_user_meta( get_current_user_id(), $metaKey, wp_json_encode( $userOptions ) );
}
/**
* Gets the user info from SEOBoost.
*
* @since 4.7.4
*
* @return array|\WP_Error The user info or a WP_Error.
*/
public function getUserInfo() {
return $this->service->getUserInfo();
}
/**
* Checks the token.
*
* @since 4.7.4
*
* @return void
*/
public function checkToken() {
$authToken = isset( $_GET['token'] ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
? sanitize_key( wp_unslash( $_GET['token'] ) ) // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
: null;
if ( $authToken ) {
$accessToken = $this->service->getAccessToken( $authToken );
if ( ! is_wp_error( $accessToken ) && ! empty( $accessToken['token'] ) ) {
$this->setAccessToken( $accessToken['token'] );
?>
<script>
// Send message to parent window.
window.opener.postMessage('seoboost-authenticated', '*');
</script>
<?php
}
}
?>
<script>
// Close window.
window.close();
</script>
<?php
die;
}
/**
* Handles the callback from the marketing site after completing authentication.
*
* @since 4.7.4
*
* @return void
*/
public function marketingSiteCallback() {
?>
<script>
// Send message to parent window.
window.opener.postMessage('seoboost-ms-logged-in', '*');
window.close();
</script>
<?php
}
/**
* Resets the logins.
*
* @since 4.7.4
*
* @return void
*/
public function resetLogins() {
// Delete access token and user options from the database.
aioseo()->core->db->delete( 'usermeta' )->whereRaw( 'meta_key LIKE \'seoboost_access_token%\'' )->run();
aioseo()->core->db->delete( 'usermeta' )->where( 'meta_key', 'seoboost_user_options' )->run();
}
/**
* Gets the report history.
*
* @since 4.7.4
*
* @return array|\WP_Error The report history.
*/
public function getReportHistory() {
return $this->service->getReportHistory();
}
/**
* Migrate Writing Assistant access tokens.
* This handles the fix for multisites where subsites all used the same workspace/account.
*
* @since 4.7.7
*
* @return void
*/
public function migrateUserData() {
$userToken = get_user_meta( get_current_user_id(), 'seoboost_access_token', true );
if ( ! empty( $userToken ) ) {
$this->setAccessToken( $userToken );
delete_user_meta( get_current_user_id(), 'seoboost_access_token' );
}
$userOptions = get_user_meta( get_current_user_id(), 'seoboost_user_options', true );
if ( ! empty( $userOptions ) ) {
$this->setUserOptions( $userOptions );
delete_user_meta( get_current_user_id(), 'seoboost_user_options' );
}
}
/**
* Refreshes user options after an error.
* This needs to run on init since service class is not available in the constructor.
*
* @since 4.7.7.2
*
* @return void
*/
public function refreshUserOptionsAfterError() {
$userOptionsFetchError = aioseo()->cache->get( 'seoboost_get_user_options_error' );
if ( $userOptionsFetchError && time() > $userOptionsFetchError ) {
aioseo()->cache->delete( 'seoboost_get_user_options_error' );
$this->refreshUserOptions();
}
}
/**
* Returns the default user options.
*
* @since 4.7.7.1
*
* @return array The default user options.
*/
private function getDefaultUserOptions() {
return [
'language' => 'en',
'country' => 'US'
];
}
} WritingAssistant/SeoBoost/Service.php 0000666 00000014005 15113050716 0013732 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\WritingAssistant\SeoBoost;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Service class for SeoBoost.
*
* @since 4.7.4
*/
class Service {
/**
* The base URL for the SeoBoost microservice.
*
* @since 4.7.4
*
* @var string
*/
private $baseUrl = 'https://app.seoboost.com/api/';
/**
* Sends the keyword to be processed.
*
* @since 4.7.4
*
* @param string $keyword The keyword.
* @param string $country The country code.
* @param string $language The language code.
* @return array|\WP_Error The response.
*/
public function processKeyword( $keyword, $country = 'US', $language = 'en' ) {
if ( empty( $keyword ) || empty( $country ) || empty( $language ) ) {
return new \WP_Error( 'service-error', __( 'Missing parameters', 'all-in-one-seo-pack' ) );
}
$reportRequest = $this->doRequest( 'waAddNewReport', [
'params' => [
'keyword' => $keyword,
'country' => $country,
'language' => $language
]
] );
if ( is_wp_error( $reportRequest ) ) {
return $reportRequest;
}
if ( empty( $reportRequest ) || empty( $reportRequest['status'] ) ) {
return new \WP_Error( 'service-error', __( 'Empty response from service', 'all-in-one-seo-pack' ) );
}
if ( 'success' !== $reportRequest['status'] ) {
return new \WP_Error( 'service-error', $reportRequest['msg'] );
}
return $reportRequest;
}
/**
* Sends a post content to be analyzed.
*
* @since 4.7.4
*
* @param string $title The title.
* @param string $description The description.
* @param string $content The content.
* @param string $reportSlug The report slug.
* @return array|\WP_Error The response.
*/
public function getContentAnalysis( $title, $description, $content, $reportSlug ) {
return $this->doRequest( 'waAnalyzeContent', [
'title' => $title,
'description' => $description,
'content' => $content,
'slug' => $reportSlug
] );
}
/**
* Gets the progress for a keyword.
*
* @since 4.7.4
*
* @param string $uuid The uuid.
* @return array|\WP_Error The progress.
*/
public function getProgressAndResult( $uuid ) {
$response = $this->doRequest( 'waGetReport', [ 'slug' => $uuid ] );
if ( is_wp_error( $response ) ) {
return $response;
}
if ( empty( $response ) ) {
return new \WP_Error( 'empty-progress-and-result', __( 'Empty progress and result.', 'all-in-one-seo-pack' ) );
}
return $response;
}
/**
* Gets the user options.
*
* @since 4.7.4
*
* @return array|\WP_Error The user options.
*/
public function getUserOptions() {
return $this->doRequest( 'waGetUserOptions' );
}
/**
* Gets the user information.
*
* @since 4.7.4
*
* @return array|\WP_Error The user information.
*/
public function getUserInfo() {
return $this->doRequest( 'waGetUserInfo' );
}
/**
* Gets the access token.
*
* @since 4.7.4
*
* @param string $authToken The auth token.
* @return array|\WP_Error The response.
*/
public function getAccessToken( $authToken ) {
return $this->doRequest( 'oauthaccess', [ 'token' => $authToken ] );
}
/**
* Refreshes the access token.
*
* @since 4.7.4
*
* @return bool Was the token refreshed?
*/
private function refreshAccessToken() {
$newAccessToken = $this->doRequest( 'waRefreshAccessToken' );
if (
is_wp_error( $newAccessToken ) ||
'success' !== $newAccessToken['status']
) {
aioseo()->writingAssistant->seoBoost->setAccessToken( '' );
return false;
}
aioseo()->writingAssistant->seoBoost->setAccessToken( $newAccessToken['token'] );
return true;
}
/**
* Sends a POST request to the microservice.
*
* @since 4.7.4
*
* @param string $path The path.
* @param array $requestBody The request body.
* @return array|\WP_Error Returns the response body or WP_Error if the request failed.
*/
private function doRequest( $path, $requestBody = [] ) {
// Prevent API requests if no access token is present.
if (
'oauthaccess' !== $path && // Except if we're getting the access token.
empty( aioseo()->writingAssistant->seoBoost->getAccessToken() )
) {
return new \WP_Error( 'service-error', __( 'Missing access token', 'all-in-one-seo-pack' ) );
}
$requestData = [
'headers' => [
'X-SeoBoost-Access-Token' => aioseo()->writingAssistant->seoBoost->getAccessToken(),
'X-SeoBoost-Domain' => aioseo()->helpers->getMultiSiteDomain(),
'Content-Type' => 'application/json'
],
'timeout' => 60,
'method' => 'GET'
];
if ( ! empty( $requestBody ) ) {
$requestData['method'] = 'POST';
$requestData['body'] = wp_json_encode( $requestBody );
}
$path = trailingslashit( $this->getUrl() ) . trailingslashit( $path );
$response = wp_remote_request( $path, $requestData );
$responseBody = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! $responseBody ) {
$response = new \WP_Error( 'service-failed', __( 'Error in the SeoBoost service. Please contact support.', 'all-in-one-seo-pack' ) );
}
if ( is_wp_error( $response ) ) {
return $response;
}
// Refresh access token if expired and redo the request.
if (
isset( $responseBody['error'] ) &&
'invalid-access-token' === $responseBody['error']
) {
if ( $this->refreshAccessToken() ) {
return $this->doRequest( $path, $requestBody );
}
}
return $responseBody;
}
/**
* Returns the URL for the Writing Assistant service.
*
* @since 4.7.4
*
* @return string The URL.
*/
public function getUrl() {
$url = $this->baseUrl;
if ( defined( 'AIOSEO_WRITING_ASSISTANT_SERVICE_URL' ) ) {
$url = AIOSEO_WRITING_ASSISTANT_SERVICE_URL;
}
return $url;
}
/**
* Gets the report history.
*
* @since 4.7.4
*
* @return array|\WP_Error
*/
public function getReportHistory() {
return $this->doRequest( 'waGetReportHistory' );
}
} WritingAssistant/WritingAssistant.php 0000666 00000001102 15113050716 0014104 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\WritingAssistant;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Main class.
*
* @since 4.7.4
*/
class WritingAssistant {
/**
* Helpers.
*
* @since 4.7.4
*
* @var Utils\Helpers
*/
public $helpers;
/**
* SeoBoost.
*
* @since 4.7.4
*
* @var SeoBoost\SeoBoost
*/
public $seoBoost;
/**
* Load our classes.
*
* @since 4.7.4
*
* @return void
*/
public function __construct() {
$this->helpers = new Utils\Helpers();
$this->seoBoost = new SeoBoost\SeoBoost();
}
} Social/Output.php 0000666 00000011065 15113050716 0007775 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Social;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
/**
* Outputs our social meta.
*
* @since 4.0.0
*/
class Output {
/**
* Checks if the current page should have social meta.
*
* @since 4.0.0
*
* @return bool Whether or not the page should have social meta.
*/
public function isAllowed() {
if ( BuddyPressIntegration::isComponentPage() ) {
return false;
}
if (
! is_front_page() &&
! is_home() &&
! is_singular() &&
! is_post_type_archive() &&
! aioseo()->helpers->isWooCommerceShopPage()
) {
return false;
}
return true;
}
/**
* Returns the Open Graph meta.
*
* @since 4.0.0
*
* @return array The Open Graph meta.
*/
public function getFacebookMeta() {
if ( ! $this->isAllowed() || ! aioseo()->options->social->facebook->general->enable ) {
return [];
}
$meta = [
'og:locale' => aioseo()->social->facebook->getLocale(),
'og:site_name' => aioseo()->helpers->encodeOutputHtml( aioseo()->social->facebook->getSiteName() ),
'og:type' => aioseo()->social->facebook->getObjectType(),
'og:title' => aioseo()->helpers->encodeOutputHtml( aioseo()->social->facebook->getTitle() ),
'og:description' => aioseo()->helpers->encodeOutputHtml( aioseo()->social->facebook->getDescription() ),
'og:url' => esc_url( aioseo()->helpers->canonicalUrl() ),
'fb:app_id' => aioseo()->options->social->facebook->advanced->appId,
'fb:admins' => implode( ',', array_map( 'trim', explode( ',', aioseo()->options->social->facebook->advanced->adminId ) ) ),
];
$image = aioseo()->social->facebook->getImage();
if ( $image ) {
$image = is_array( $image ) ? $image[0] : $image;
$image = aioseo()->helpers->makeUrlAbsolute( $image );
$image = set_url_scheme( esc_url( $image ) );
$meta += [
'og:image' => $image,
'og:image:secure_url' => is_ssl() ? $image : '',
'og:image:width' => aioseo()->social->facebook->getImageWidth(),
'og:image:height' => aioseo()->social->facebook->getImageHeight(),
];
}
$video = aioseo()->social->facebook->getVideo();
if ( $video ) {
$video = set_url_scheme( esc_url( $video ) );
$meta += [
'og:video' => $video,
'og:video:secure_url' => is_ssl() ? $video : '',
'og:video:width' => aioseo()->social->facebook->getVideoWidth(),
'og:video:height' => aioseo()->social->facebook->getVideoHeight(),
];
}
if ( ! empty( $meta['og:type'] ) && 'article' === $meta['og:type'] ) {
$meta += [
'article:section' => aioseo()->social->facebook->getSection(),
'article:tag' => aioseo()->social->facebook->getArticleTags(),
'article:published_time' => aioseo()->social->facebook->getPublishedTime(),
'article:modified_time' => aioseo()->social->facebook->getModifiedTime(),
'article:publisher' => aioseo()->social->facebook->getPublisher(),
'article:author' => aioseo()->social->facebook->getAuthor()
];
}
return array_filter( apply_filters( 'aioseo_facebook_tags', $meta ) );
}
/**
* Returns the Twitter meta.
*
* @since 4.0.0
*
* @return array The Twitter meta.
*/
public function getTwitterMeta() {
if ( ! $this->isAllowed() || ! aioseo()->options->social->twitter->general->enable ) {
return [];
}
$meta = [
'twitter:card' => aioseo()->social->twitter->getCardType(),
'twitter:site' => aioseo()->social->twitter->prepareUsername( aioseo()->social->twitter->getTwitterUrl() ),
'twitter:title' => aioseo()->helpers->encodeOutputHtml( aioseo()->social->twitter->getTitle() ),
'twitter:description' => aioseo()->helpers->encodeOutputHtml( aioseo()->social->twitter->getDescription() ),
'twitter:creator' => aioseo()->social->twitter->getCreator()
];
$image = aioseo()->social->twitter->getImage();
if ( $image ) {
$image = is_array( $image ) ? $image[0] : $image;
$image = aioseo()->helpers->makeUrlAbsolute( $image );
// Set the twitter image meta.
$meta['twitter:image'] = $image;
}
if ( is_singular() ) {
$additionalData = apply_filters( 'aioseo_social_twitter_additional_data', aioseo()->social->twitter->getAdditionalData() );
if ( $additionalData ) {
$i = 1;
foreach ( $additionalData as $data ) {
$meta[ "twitter:label$i" ] = $data['label'];
$meta[ "twitter:data$i" ] = $data['value'];
$i++;
}
}
}
return array_filter( apply_filters( 'aioseo_twitter_tags', $meta ) );
}
} Social/Facebook.php 0000666 00000040211 15113050716 0010201 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Social;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Traits;
/**
* Handles the Open Graph meta.
*
* @since 4.0.0
*/
class Facebook {
use Traits\SocialProfiles;
/**
* Returns the Open Graph image URL.
*
* @since 4.0.0
*
* @param int $postId The post ID (optional).
* @return string The image URL.
*/
public function getImage( $postId = null ) {
$post = aioseo()->helpers->getPost( $postId );
if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
$image = aioseo()->options->social->facebook->homePage->image;
if ( empty( $image ) ) {
$image = aioseo()->social->image->getImage( 'facebook', aioseo()->options->social->facebook->general->defaultImageSourcePosts, $post );
}
return $image;
}
$metaData = aioseo()->meta->metaData->getMetaData( $post );
$image = '';
if ( ! empty( $metaData ) ) {
$imageSource = ! empty( $metaData->og_image_type ) && 'default' !== $metaData->og_image_type
? $metaData->og_image_type
: aioseo()->options->social->facebook->general->defaultImageSourcePosts;
$image = aioseo()->social->image->getImage( 'facebook', $imageSource, $post );
}
// Since we could be on an archive page, let's check again for that default image.
if ( ! $image ) {
$image = aioseo()->social->image->getImage( 'facebook', 'default' );
}
if ( ! $image ) {
$image = aioseo()->helpers->getSiteLogoUrl();
}
// Allow users to control the default image per post type.
return apply_filters(
'aioseo_opengraph_default_image',
$image,
[
$post,
$this->getObjectType()
]
);
}
/**
* Returns the width of the Open Graph image.
*
* @since 4.0.0
*
* @return string The image width.
*/
public function getImageWidth() {
if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
$width = aioseo()->options->social->facebook->homePage->imageWidth;
return $width ? $width : aioseo()->options->social->facebook->general->defaultImagePostsWidth;
}
$metaData = aioseo()->meta->metaData->getMetaData();
if ( ! empty( $metaData->og_custom_image_width ) ) {
return $metaData->og_custom_image_width;
}
$image = $this->getImage();
if ( is_array( $image ) ) {
return $image[1];
}
return aioseo()->options->social->facebook->general->defaultImagePostsWidth;
}
/**
* Returns the height of the Open Graph image.
*
* @since 4.0.0
*
* @return string The image height.
*/
public function getImageHeight() {
if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
$height = aioseo()->options->social->facebook->homePage->imageHeight;
return $height ? $height : aioseo()->options->social->facebook->general->defaultImagePostsHeight;
}
$metaData = aioseo()->meta->metaData->getMetaData();
if ( ! empty( $metaData->og_custom_image_height ) ) {
return $metaData->og_custom_image_height;
}
$image = $this->getImage();
if ( is_array( $image ) ) {
return $image[2];
}
return aioseo()->options->social->facebook->general->defaultImagePostsHeight;
}
/**
* Returns the Open Graph video URL.
*
* @since 4.0.0
*
* @return string The video URL.
*/
public function getVideo() {
$metaData = aioseo()->meta->metaData->getMetaData();
return ! empty( $metaData->og_video ) ? $metaData->og_video : '';
}
/**
* Returns the width of the video.
*
* @since 4.0.0
*
* @return string The video width.
*/
public function getVideoWidth() {
$metaData = aioseo()->meta->metaData->getMetaData();
return ! empty( $metaData->og_video_width ) ? $metaData->og_video_width : '';
}
/**
* Returns the height of the video.
*
* @since 4.0.0
*
* @return string The video height.
*/
public function getVideoHeight() {
$metaData = aioseo()->meta->metaData->getMetaData();
return ! empty( $metaData->og_video_height ) ? $metaData->og_video_height : '';
}
/**
* Returns the site name.
*
* @since 4.0.0
*
* @return string The site name.
*/
public function getSiteName() {
$title = aioseo()->helpers->decodeHtmlEntities( aioseo()->tags->replaceTags( aioseo()->options->social->facebook->general->siteName, get_the_ID() ) );
if ( ! $title ) {
$title = aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
}
return wp_strip_all_tags( $title );
}
/**
* Returns the Open Graph object type.
*
* @since 4.0.0
*
* @return string The object type.
*/
public function getObjectType() {
if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
$type = aioseo()->options->social->facebook->homePage->objectType;
return $type ? $type : 'website';
}
if ( is_post_type_archive() ) {
return 'website';
}
$post = aioseo()->helpers->getPost();
$metaData = aioseo()->meta->metaData->getMetaData( $post );
if ( ! empty( $metaData->og_object_type ) && 'default' !== $metaData->og_object_type ) {
return $metaData->og_object_type;
}
$postType = get_post_type();
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
$defaultObjectType = $dynamicOptions->social->facebook->general->postTypes->has( $postType )
? $dynamicOptions->social->facebook->general->postTypes->$postType->objectType
: '';
return ! empty( $defaultObjectType ) ? $defaultObjectType : 'article';
}
/**
* Returns the Open Graph title for the current page.
*
* @since 4.0.0
*
* @param \WP_Post|integer $post The post object or ID (optional).
* @return string The Open Graph title.
*/
public function getTitle( $post = null ) {
if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
$title = aioseo()->meta->title->helpers->prepare( aioseo()->options->social->facebook->homePage->title );
return $title ? $title : aioseo()->meta->title->getTitle();
}
$post = aioseo()->helpers->getPost( $post );
$metaData = aioseo()->meta->metaData->getMetaData( $post );
$title = '';
if ( ! empty( $metaData->og_title ) ) {
$title = aioseo()->meta->title->helpers->prepare( $metaData->og_title );
}
if ( is_post_type_archive() ) {
$postType = get_queried_object();
if ( is_a( $postType, 'WP_Post_Type' ) ) {
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
if ( $dynamicOptions->searchAppearance->archives->has( $postType->name ) ) {
$title = aioseo()->meta->title->helpers->prepare( aioseo()->dynamicOptions->searchAppearance->archives->{ $postType->name }->title );
}
}
}
return $title
? $title
: (
$post
? aioseo()->meta->title->getPostTitle( $post )
: $title
);
}
/**
* Returns the Open Graph description.
*
* @since 4.0.0
*
* @param \WP_Post|integer $post The post object or ID (optional).
* @return string The Open Graph description.
*/
public function getDescription( $post = null ) {
if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
$description = aioseo()->meta->description->helpers->prepare( aioseo()->options->social->facebook->homePage->description );
return $description ? $description : aioseo()->meta->description->getDescription();
}
$post = aioseo()->helpers->getPost( $post );
$metaData = aioseo()->meta->metaData->getMetaData( $post );
$description = '';
if ( ! empty( $metaData->og_description ) ) {
$description = aioseo()->meta->description->helpers->prepare( $metaData->og_description );
}
if ( is_post_type_archive() ) {
$postType = get_queried_object();
if ( is_a( $postType, 'WP_Post_Type' ) ) {
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
if ( $dynamicOptions->searchAppearance->archives->has( $postType->name ) ) {
$description = aioseo()->meta->description->helpers->prepare( aioseo()->dynamicOptions->searchAppearance->archives->{ $postType->name }->metaDescription );
}
}
}
return $description
? $description
: (
$post
? aioseo()->meta->description->getPostDescription( $post )
: $description
);
}
/**
* Returns the Open Graph article section name.
*
* @since 4.0.0
*
* @return string The article section name.
*/
public function getSection() {
$metaData = aioseo()->meta->metaData->getMetaData();
return ! empty( $metaData->og_article_section ) ? $metaData->og_article_section : '';
}
/**
* Returns the Open Graph publisher URL.
*
* @since 4.0.0
*
* @return string The Open Graph publisher URL.
*/
public function getPublisher() {
if ( ! aioseo()->options->social->profiles->sameUsername->enable ) {
return aioseo()->options->social->profiles->urls->facebookPageUrl;
}
$username = aioseo()->options->social->profiles->sameUsername->username;
return ( $username && in_array( 'facebookPageUrl', aioseo()->options->social->profiles->sameUsername->included, true ) )
? 'https://facebook.com/' . $username
: '';
}
/**
* Returns the published time.
*
* @since 4.0.0
*
* @return string The published time.
*/
public function getPublishedTime() {
$post = aioseo()->helpers->getPost();
return $post ? aioseo()->helpers->dateTimeToIso8601( $post->post_date_gmt ) : '';
}
/**
* Returns the last modified time.
*
* @since 4.0.0
*
* @return string The last modified time.
*/
public function getModifiedTime() {
$post = aioseo()->helpers->getPost();
return $post ? aioseo()->helpers->dateTimeToIso8601( $post->post_modified_gmt ) : '';
}
/**
* Returns the Open Graph author.
*
* @since 4.0.0
*
* @return string The Open Graph author.
*/
public function getAuthor() {
$post = aioseo()->helpers->getPost();
if ( ! is_a( $post, 'WP_Post' ) || ! aioseo()->options->social->facebook->general->showAuthor ) {
return '';
}
$author = '';
$userProfiles = $this->getUserProfiles( $post->post_author );
if ( ! empty( $userProfiles['facebookPageUrl'] ) ) {
$author = $userProfiles['facebookPageUrl'];
}
if ( empty( $author ) ) {
$author = aioseo()->options->social->facebook->advanced->authorUrl;
}
return $author;
}
/**
* Returns the Open Graph article tags.
*
* @since 4.0.0
*
* @return array An array of unique keywords.
*/
public function getArticleTags() {
$post = aioseo()->helpers->getPost();
$metaData = aioseo()->meta->metaData->getMetaData( $post );
$tags = ! empty( $metaData->og_article_tags ) ? aioseo()->meta->keywords->extractMetaKeywords( $metaData->og_article_tags ) : [];
if (
$post &&
aioseo()->options->social->facebook->advanced->enable &&
aioseo()->options->social->facebook->advanced->generateArticleTags
) {
if ( aioseo()->options->social->facebook->advanced->useKeywordsInTags ) {
$keywords = aioseo()->meta->keywords->getKeywords();
$keywords = aioseo()->tags->parseCustomFields( $keywords );
$keywords = aioseo()->meta->keywords->keywordStringToList( $keywords );
$tags = array_merge( $tags, $keywords );
}
if ( aioseo()->options->social->facebook->advanced->useCategoriesInTags ) {
$tags = array_merge( $tags, aioseo()->helpers->getAllCategories( $post->ID ) );
}
if ( aioseo()->options->social->facebook->advanced->usePostTagsInTags ) {
$tags = array_merge( $tags, aioseo()->helpers->getAllTags( $post->ID ) );
}
}
return aioseo()->meta->keywords->getUniqueKeywords( $tags, false );
}
/**
* Retreive the locale.
*
* @since 4.1.4
*
* @return string The locale.
*/
public function getLocale() {
$locale = get_locale();
// These are the locales FB supports.
$validLocales = [
'af_ZA', // Afrikaans.
'ak_GH', // Akan.
'am_ET', // Amharic.
'ar_AR', // Arabic.
'as_IN', // Assamese.
'ay_BO', // Aymara.
'az_AZ', // Azerbaijani.
'be_BY', // Belarusian.
'bg_BG', // Bulgarian.
'bp_IN', // Bhojpuri.
'bn_IN', // Bengali.
'br_FR', // Breton.
'bs_BA', // Bosnian.
'ca_ES', // Catalan.
'cb_IQ', // Sorani Kurdish.
'ck_US', // Cherokee.
'co_FR', // Corsican.
'cs_CZ', // Czech.
'cx_PH', // Cebuano.
'cy_GB', // Welsh.
'da_DK', // Danish.
'de_DE', // German.
'el_GR', // Greek.
'en_GB', // English (UK).
'en_PI', // English (Pirate).
'en_UD', // English (Upside Down).
'en_US', // English (US).
'em_ZM',
'eo_EO', // Esperanto.
'es_ES', // Spanish (Spain).
'es_LA', // Spanish.
'es_MX', // Spanish (Mexico).
'et_EE', // Estonian.
'eu_ES', // Basque.
'fa_IR', // Persian.
'fb_LT', // Leet Speak.
'ff_NG', // Fulah.
'fi_FI', // Finnish.
'fo_FO', // Faroese.
'fr_CA', // French (Canada).
'fr_FR', // French (France).
'fy_NL', // Frisian.
'ga_IE', // Irish.
'gl_ES', // Galician.
'gn_PY', // Guarani.
'gu_IN', // Gujarati.
'gx_GR', // Classical Greek.
'ha_NG', // Hausa.
'he_IL', // Hebrew.
'hi_IN', // Hindi.
'hr_HR', // Croatian.
'hu_HU', // Hungarian.
'ht_HT', // Haitian Creole.
'hy_AM', // Armenian.
'id_ID', // Indonesian.
'ig_NG', // Igbo.
'is_IS', // Icelandic.
'it_IT', // Italian.
'ik_US',
'iu_CA',
'ja_JP', // Japanese.
'ja_KS', // Japanese (Kansai).
'jv_ID', // Javanese.
'ka_GE', // Georgian.
'kk_KZ', // Kazakh.
'km_KH', // Khmer.
'kn_IN', // Kannada.
'ko_KR', // Korean.
'ks_IN', // Kashmiri.
'ku_TR', // Kurdish (Kurmanji).
'ky_KG', // Kyrgyz.
'la_VA', // Latin.
'lg_UG', // Ganda.
'li_NL', // Limburgish.
'ln_CD', // Lingala.
'lo_LA', // Lao.
'lt_LT', // Lithuanian.
'lv_LV', // Latvian.
'mg_MG', // Malagasy.
'mi_NZ', // Maori.
'mk_MK', // Macedonian.
'ml_IN', // Malayalam.
'mn_MN', // Mongolian.
'mr_IN', // Marathi.
'ms_MY', // Malay.
'mt_MT', // Maltese.
'my_MM', // Burmese.
'nb_NO', // Norwegian (bokmal).
'nd_ZW', // Ndebele.
'ne_NP', // Nepali.
'nl_BE', // Dutch (Belgie).
'nl_NL', // Dutch.
'nn_NO', // Norwegian (nynorsk).
'nr_ZA', // Southern Ndebele.
'ns_ZA', // Northern Sotho.
'ny_MW', // Chewa.
'om_ET', // Oromo.
'or_IN', // Oriya.
'pa_IN', // Punjabi.
'pl_PL', // Polish.
'ps_AF', // Pashto.
'pt_BR', // Portuguese (Brazil).
'pt_PT', // Portuguese (Portugal).
'qc_GT', // Quiché.
'qu_PE', // Quechua.
'qr_GR',
'qz_MM', // Burmese (Zawgyi).
'rm_CH', // Romansh.
'ro_RO', // Romanian.
'ru_RU', // Russian.
'rw_RW', // Kinyarwanda.
'sa_IN', // Sanskrit.
'sc_IT', // Sardinian.
'se_NO', // Northern Sami.
'si_LK', // Sinhala.
'su_ID', // Sundanese.
'sk_SK', // Slovak.
'sl_SI', // Slovenian.
'sn_ZW', // Shona.
'so_SO', // Somali.
'sq_AL', // Albanian.
'sr_RS', // Serbian.
'ss_SZ', // Swazi.
'st_ZA', // Southern Sotho.
'sv_SE', // Swedish.
'sw_KE', // Swahili.
'sy_SY', // Syriac.
'sz_PL', // Silesian.
'ta_IN', // Tamil.
'te_IN', // Telugu.
'tg_TJ', // Tajik.
'th_TH', // Thai.
'tk_TM', // Turkmen.
'tl_PH', // Filipino.
'tl_ST', // Klingon.
'tn_BW', // Tswana.
'tr_TR', // Turkish.
'ts_ZA', // Tsonga.
'tt_RU', // Tatar.
'tz_MA', // Tamazight.
'uk_UA', // Ukrainian.
'ur_PK', // Urdu.
'uz_UZ', // Uzbek.
've_ZA', // Venda.
'vi_VN', // Vietnamese.
'wo_SN', // Wolof.
'xh_ZA', // Xhosa.
'yi_DE', // Yiddish.
'yo_NG', // Yoruba.
'zh_CN', // Simplified Chinese (China).
'zh_HK', // Traditional Chinese (Hong Kong).
'zh_TW', // Traditional Chinese (Taiwan).
'zu_ZA', // Zulu.
'zz_TR', // Zazaki.
];
// Catch some weird locales served out by WP that are not easily doubled up.
$fixLocales = [
'ca' => 'ca_ES',
'en' => 'en_US',
'el' => 'el_GR',
'et' => 'et_EE',
'ja' => 'ja_JP',
'sq' => 'sq_AL',
'uk' => 'uk_UA',
'vi' => 'vi_VN',
'zh' => 'zh_CN',
];
if ( isset( $fixLocales[ $locale ] ) ) {
$locale = $fixLocales[ $locale ];
}
// Convert locales like "es" to "es_ES", in case that works for the given locale (sometimes it does).
if ( 2 === strlen( $locale ) ) {
$locale = strtolower( $locale ) . '_' . strtoupper( $locale );
}
// Check to see if the locale is a valid FB one, if not, use en_US as a fallback.
if ( ! in_array( $locale, $validLocales, true ) ) {
$locale = strtolower( substr( $locale, 0, 2 ) ) . '_' . strtoupper( substr( $locale, 0, 2 ) );
if ( ! in_array( $locale, $validLocales, true ) ) {
$locale = 'en_US';
}
}
return apply_filters( 'aioseo_og_locale', $locale );
}
} Social/Social.php 0000666 00000010213 15113050716 0007701 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Social;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the Social Meta.
*
* @package AIOSEO\Plugin\Common\Social
*
* @since 4.0.0
*/
class Social {
/**
* The name of the action to bust the OG cache.
*
* @since 4.2.0
*
* @var string
*/
private $bustOgCacheActionName = 'aioseo_og_cache_bust_post';
/**
* Image class instance.
*
* @since 4.2.7
*
* @var Image
*/
public $image = null;
/**
* Facebook class instance.
*
* @since 4.2.7
*
* @var Facebook
*/
public $facebook = null;
/**
* Twitter class instance.
*
* @since 4.2.7
*
* @var Twitter
*/
public $twitter = null;
/**
* Output class instance.
*
* @since 4.2.7
*
* @var Output
*/
public $output = null;
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
$this->image = new Image();
if ( wp_doing_ajax() || wp_doing_cron() ) {
return;
}
$this->facebook = new Facebook();
$this->twitter = new Twitter();
$this->output = new Output();
$this->hooks();
}
/**
* Registers our hooks.
*
* @since 4.0.0
*/
protected function hooks() {
add_action( $this->bustOgCacheActionName, [ $this, 'bustOgCachePost' ] );
// To avoid duplicate sets of meta tags.
add_filter( 'jetpack_enable_open_graph', '__return_false' );
if ( ! is_admin() ) {
add_filter( 'language_attributes', [ $this, 'addAttributes' ] );
return;
}
// Forces a refresh of the Facebook cache.
add_action( 'post_updated', [ $this, 'scheduleBustOgCachePost' ], 10, 2 );
}
/**
* Adds our attributes to the registered language attributes.
*
* @since 4.0.0
* @version 4.4.5 Adds trim function the html tag removing empty spaces.
*
* @param string $htmlTag The 'html' tag as a string.
* @return string The filtered 'html' tag as a string.
*/
public function addAttributes( $htmlTag ) {
if ( ! aioseo()->options->social->facebook->general->enable ) {
return $htmlTag;
}
$attributes = apply_filters( 'aioseo_opengraph_attributes', [ 'prefix="og: https://ogp.me/ns#"' ] );
foreach ( $attributes as $attr ) {
if ( strpos( $htmlTag, $attr ) === false ) {
$htmlTag .= " $attr ";
}
}
return trim( $htmlTag );
}
/**
* Schedule a ping to bust the OG cache.
*
* @since 4.2.0
*
* @param int $postId The post ID.
* @param \WP_Post $post The post object.
* @return void
*/
public function scheduleBustOgCachePost( $postId, $post = null ) {
if ( ! aioseo()->helpers->isSbCustomFacebookFeedActive() || ! aioseo()->helpers->isValidPost( $post ) ) {
return;
}
if ( aioseo()->actionScheduler->isScheduled( $this->bustOgCacheActionName, [ 'postId' => $postId ] ) ) {
return;
}
// Schedule the new ping.
aioseo()->actionScheduler->scheduleAsync( $this->bustOgCacheActionName, [ 'postId' => $postId ] );
}
/**
* Pings Facebook and asks them to bust the OG cache for a particular post.
*
* @since 4.2.0
*
* @see https://developers.facebook.com/docs/sharing/opengraph/using-objects#update
*
* @param int $postId The post ID.
* @return void
*/
public function bustOgCachePost( $postId ) {
$post = get_post( $postId );
$customAccessToken = apply_filters( 'aioseo_facebook_access_token', '' );
if (
! aioseo()->helpers->isValidPost( $post ) ||
( ! aioseo()->helpers->isSbCustomFacebookFeedActive() && ! $customAccessToken )
) {
return;
}
$permalink = get_permalink( $postId );
$this->bustOgCacheHelper( $permalink );
}
/**
* Helper function for bustOgCache().
*
* @since 4.2.0
*
* @param string $permalink The permalink.
* @return void
*/
protected function bustOgCacheHelper( $permalink ) {
$accessToken = aioseo()->helpers->getSbAccessToken();
$accessToken = apply_filters( 'aioseo_facebook_access_token', $accessToken );
if ( ! $accessToken ) {
return;
}
$url = sprintf(
'https://graph.facebook.com/?%s',
http_build_query(
[
'id' => $permalink,
'scrape' => true,
'access_token' => $accessToken
]
)
);
wp_remote_post( $url, [ 'blocking' => false ] );
}
} Social/Image.php 0000666 00000017435 15113050716 0007526 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Social;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Handles the Open Graph and Twitter Image.
*
* @since 4.0.0
*/
class Image {
/**
* The type of image ("facebook" or "twitter").
*
* @since 4.1.6.2
*
* @var string
*/
protected $type;
/**
* The post object.
*
* @since 4.1.6.2
*
* @var \WP_Post
*/
private $post;
/**
* The default thumbnail size.
*
* @since 4.0.0
*
* @var string
*/
protected $thumbnailSize;
/**
* Whether or not to use the cached images.
*
* @since 4.1.6
*
* @var boolean
*/
public $useCache = true;
/**
* Returns the Facebook or Twitter image.
*
* @since 4.0.0
*
* @param string $type The type ("Facebook" or "Twitter").
* @param string $imageSource The image source.
* @param \WP_Post|null $post The post object.
* @return string|array The image data.
*/
public function getImage( $type, $imageSource, $post = null ) {
$this->type = $type;
$this->post = $post;
$this->thumbnailSize = apply_filters( 'aioseo_thumbnail_size', 'fullsize' );
$hash = md5( wp_json_encode( [ $type, $imageSource, $post ] ) );
static $images = [];
if ( isset( $images[ $hash ] ) ) {
return $images[ $hash ];
}
if ( 'auto' === $imageSource && aioseo()->helpers->getPostPageBuilderName( $post->ID ) ) {
$imageSource = 'default';
}
if ( is_a( $this->post, 'WP_Post' ) ) {
switch ( $imageSource ) {
case 'featured':
$image = $this->getFeaturedImage();
break;
case 'attach':
$image = $this->getFirstAttachedImage();
break;
case 'content':
$image = $this->getFirstImageInContent();
break;
case 'author':
$image = $this->getAuthorAvatar();
break;
case 'auto':
$image = $this->getFirstAvailableImage();
break;
case 'custom':
$image = $this->getCustomFieldImage();
break;
case 'custom_image':
$metaData = aioseo()->meta->metaData->getMetaData( $post );
if ( empty( $metaData ) ) {
break;
}
$image = 'facebook' === strtolower( $this->type )
? $metaData->og_image_custom_url
: $metaData->twitter_image_custom_url;
break;
case 'default':
default:
$image = aioseo()->options->social->{$this->type}->general->defaultImagePosts;
}
}
if ( empty( $image ) ) {
$image = aioseo()->options->social->{$this->type}->general->defaultImagePosts;
}
if ( is_array( $image ) ) {
$images[ $hash ] = $image;
return $images[ $hash ];
}
$imageWithoutDimensions = aioseo()->helpers->removeImageDimensions( $image );
$attachmentId = aioseo()->helpers->attachmentUrlToPostId( $imageWithoutDimensions );
$images[ $hash ] = $attachmentId ? wp_get_attachment_image_src( $attachmentId, $this->thumbnailSize ) : $image;
return $images[ $hash ];
}
/**
* Returns the Featured Image for the post.
*
* @since 4.0.0
*
* @return array The image data.
*/
private function getFeaturedImage() {
$cachedImage = $this->getCachedImage();
if ( $cachedImage ) {
return $cachedImage;
}
$imageId = get_post_thumbnail_id( $this->post->ID );
return $imageId ? wp_get_attachment_image_src( $imageId, $this->thumbnailSize ) : '';
}
/**
* Returns the first attached image.
*
* @since 4.0.0
*
* @return string The image data.
*/
private function getFirstAttachedImage() {
$cachedImage = $this->getCachedImage();
if ( $cachedImage ) {
return $cachedImage;
}
if ( 'attachment' === get_post_type( $this->post->ID ) ) {
return wp_get_attachment_image_src( $this->post->ID, $this->thumbnailSize );
}
$attachments = get_children(
[
'post_parent' => $this->post->ID,
'post_status' => 'inherit',
'post_type' => 'attachment',
'post_mime_type' => 'image',
]
);
return $attachments && count( $attachments ) ? wp_get_attachment_image_src( array_values( $attachments )[0]->ID, $this->thumbnailSize ) : '';
}
/**
* Returns the first image found in the post content.
*
* @since 4.0.0
*
* @return string The image URL.
*/
private function getFirstImageInContent() {
$cachedImage = $this->getCachedImage();
if ( $cachedImage ) {
return $cachedImage;
}
$postContent = aioseo()->helpers->getPostContent( $this->post );
preg_match_all( '|<img.*?src=[\'"](.*?)[\'"].*?>|i', (string) $postContent, $matches ); // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
// Ignore cover block background image - WP >= 5.7.
if ( ! empty( $matches[0] ) && apply_filters( 'aioseo_social_image_ignore_cover_block', true, $this->post, $matches ) ) {
foreach ( $matches[0] as $key => $match ) {
if ( false !== stripos( $match, 'wp-block-cover__image-background' ) ) {
unset( $matches[1][ $key ] );
}
}
}
return ! empty( $matches[1] ) ? current( $matches[1] ) : '';
}
/**
* Returns the author avatar.
*
* @since 4.0.0
*
* @return string The image URL.
*/
private function getAuthorAvatar() {
$avatar = get_avatar( $this->post->post_author, 300 );
preg_match( "/src='(.*?)'/i", (string) $avatar, $matches );
return ! empty( $matches[1] ) ? $matches[1] : '';
}
/**
* Returns the first available image.
*
* @since 4.0.0
*
* @return string The image URL.
*/
private function getFirstAvailableImage() {
// Disable the cache.
$this->useCache = false;
$image = $this->getCustomFieldImage();
if ( ! $image ) {
$image = $this->getFeaturedImage();
}
if ( ! $image ) {
$image = $this->getFirstAttachedImage();
}
if ( ! $image ) {
$image = $this->getFirstImageInContent();
}
if ( ! $image && 'twitter' === strtolower( $this->type ) ) {
$image = aioseo()->options->social->twitter->homePage->image;
}
// Enable the cache.
$this->useCache = true;
return $image ? $image : aioseo()->options->social->facebook->homePage->image;
}
/**
* Returns the image from a custom field.
*
* @since 4.0.0
*
* @return string The image URL.
*/
private function getCustomFieldImage() {
$cachedImage = $this->getCachedImage();
if ( $cachedImage ) {
return $cachedImage;
}
$prefix = 'facebook' === strtolower( $this->type ) ? 'og_' : 'twitter_';
$aioseoPost = Models\Post::getPost( $this->post->ID );
$customFields = ! empty( $aioseoPost->{ $prefix . 'image_custom_fields' } )
? $aioseoPost->{ $prefix . 'image_custom_fields' }
: aioseo()->options->social->{$this->type}->general->customFieldImagePosts;
if ( ! $customFields ) {
return '';
}
$customFields = explode( ',', $customFields );
foreach ( $customFields as $customField ) {
$image = get_post_meta( $this->post->ID, $customField, true );
if ( ! empty( $image ) ) {
$image = is_array( $image ) ? $image[0] : $image;
return is_numeric( $image )
? wp_get_attachment_image_src( $image, $this->thumbnailSize )
: $image;
}
}
return '';
}
/**
* Returns the cached image if there is one.
*
* @since 4.1.6.2
*
* @param \WP_Term $object The object for which we need to get the cached image.
* @return string|array The image URL or data.
*/
protected function getCachedImage( $object = null ) {
if ( null === $object ) {
// This isn't null if we call it from the Pro class.
$object = $this->post;
}
$metaData = aioseo()->meta->metaData->getMetaData( $object );
switch ( $this->type ) {
case 'facebook':
if ( ! empty( $metaData->og_image_url ) && $this->useCache ) {
return aioseo()->meta->metaData->getCachedOgImage( $metaData );
}
break;
case 'twitter':
if ( ! empty( $metaData->twitter_image_url ) && $this->useCache ) {
return $metaData->twitter_image_url;
}
break;
default:
break;
}
return '';
}
} Social/Twitter.php 0000666 00000017724 15113050716 0010147 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Social;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Traits;
/**
* Handles the Twitter meta.
*
* @since 4.0.0
*/
class Twitter {
use Traits\SocialProfiles;
/**
* Returns the Twitter URL for the site.
*
* @since 4.0.0
*
* @return string The Twitter URL.
*/
public function getTwitterUrl() {
if ( ! aioseo()->options->social->profiles->sameUsername->enable ) {
return aioseo()->options->social->profiles->urls->twitterUrl;
}
$userName = aioseo()->options->social->profiles->sameUsername->username;
return ( $userName && in_array( 'twitterUrl', aioseo()->options->social->profiles->sameUsername->included, true ) )
? 'https://x.com/' . $userName
: '';
}
/**
* Returns the Twitter card type.
*
* @since 4.0.0
*
* @return string $card The card type.
*/
public function getCardType() {
if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
return aioseo()->options->social->twitter->homePage->cardType;
}
$metaData = aioseo()->meta->metaData->getMetaData();
return ! empty( $metaData->twitter_card ) && 'default' !== $metaData->twitter_card ? $metaData->twitter_card : aioseo()->options->social->twitter->general->defaultCardType;
}
/**
* Returns the Twitter creator.
*
* @since 4.0.0
*
* @return string The creator.
*/
public function getCreator() {
$post = aioseo()->helpers->getPost();
if (
! is_a( $post, 'WP_Post' ) ||
! post_type_supports( $post->post_type, 'author' ) ||
! aioseo()->options->social->twitter->general->showAuthor
) {
return '';
}
$author = '';
$userProfiles = $this->getUserProfiles( $post->post_author );
if ( ! empty( $userProfiles['twitterUrl'] ) ) {
$author = $userProfiles['twitterUrl'];
}
if ( empty( $author ) ) {
$author = aioseo()->social->twitter->getTwitterUrl();
}
$author = aioseo()->social->twitter->prepareUsername( $author );
return $author;
}
/**
* Returns the Twitter image URL.
*
* @since 4.0.0
*
* @param int $postId The post ID (optional).
* @return string The image URL.
*/
public function getImage( $postId = null ) {
$post = aioseo()->helpers->getPost( $postId );
if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
$image = aioseo()->options->social->twitter->homePage->image;
if ( empty( $image ) ) {
$image = aioseo()->options->social->facebook->homePage->image;
}
if ( empty( $image ) ) {
$image = aioseo()->social->image->getImage( 'twitter', aioseo()->options->social->twitter->general->defaultImageSourcePosts, $post );
}
return $image ? $image : aioseo()->social->facebook->getImage();
}
$metaData = aioseo()->meta->metaData->getMetaData( $post );
if ( ! empty( $metaData->twitter_use_og ) ) {
return aioseo()->social->facebook->getImage();
}
$image = '';
if ( ! empty( $metaData ) ) {
$imageSource = ! empty( $metaData->twitter_image_type ) && 'default' !== $metaData->twitter_image_type
? $metaData->twitter_image_type
: aioseo()->options->social->twitter->general->defaultImageSourcePosts;
$image = aioseo()->social->image->getImage( 'twitter', $imageSource, $post );
}
return $image ? $image : aioseo()->social->facebook->getImage();
}
/**
* Returns the Twitter title for the current page.
*
* @since 4.0.0
*
* @param \WP_Post|integer $post The post object or ID (optional).
* @return string The Twitter title.
*/
public function getTitle( $post = null ) {
if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
$title = aioseo()->meta->title->helpers->prepare( aioseo()->options->social->twitter->homePage->title );
return $title ? $title : aioseo()->social->facebook->getTitle( $post );
}
$post = aioseo()->helpers->getPost( $post );
$metaData = aioseo()->meta->metaData->getMetaData( $post );
if ( ! empty( $metaData->twitter_use_og ) ) {
return aioseo()->social->facebook->getTitle( $post );
}
$title = '';
if ( ! empty( $metaData->twitter_title ) ) {
$title = aioseo()->meta->title->helpers->prepare( $metaData->twitter_title );
}
return $title ? $title : aioseo()->social->facebook->getTitle( $post );
}
/**
* Returns the Twitter description for the current page.
*
* @since 4.0.0
*
* @param \WP_Post|integer $post The post object or ID (optional).
* @return string The Twitter description.
*/
public function getDescription( $post = null ) {
if ( is_home() && 'posts' === get_option( 'show_on_front' ) ) {
$description = aioseo()->meta->description->helpers->prepare( aioseo()->options->social->twitter->homePage->description );
return $description ? $description : aioseo()->social->facebook->getDescription( $post );
}
$post = aioseo()->helpers->getPost( $post );
$metaData = aioseo()->meta->metaData->getMetaData( $post );
if ( ! empty( $metaData->twitter_use_og ) ) {
return aioseo()->social->facebook->getDescription( $post );
}
$description = '';
if ( ! empty( $metaData->twitter_description ) ) {
$description = aioseo()->meta->description->helpers->prepare( $metaData->twitter_description );
}
return $description ? $description : aioseo()->social->facebook->getDescription( $post );
}
/**
* Prepare twitter username for public display.
*
* We do things like strip out the URL, etc and return just (at)username.
* At the moment, we'll check for 1 of 3 things... (at)username, username, and https://x.com/username.
*
* @since 4.0.0
*
* @param string $profile Twitter username.
* @param boolean $includeAt Whether or not ot include the @ sign.
* @return string Full Twitter username.
*/
public function prepareUsername( $profile, $includeAt = true ) {
if ( ! $profile ) {
return $profile;
}
$profile = (string) $profile;
if ( preg_match( '/^(\@)?[A-Za-z0-9_]+$/', (string) $profile ) ) {
if ( '@' !== $profile[0] && $includeAt ) {
$profile = '@' . $profile;
} elseif ( '@' === $profile[0] && ! $includeAt ) {
$profile = ltrim( $profile, '@' );
}
}
if ( strpos( $profile, 'twitter.com' ) || strpos( $profile, 'x.com' ) ) {
$profile = esc_url( $profile );
// Extract the twitter username from the URL.
$parsedTwitterProfile = wp_parse_url( $profile );
$path = $parsedTwitterProfile['path'];
$pathParts = explode( '/', $path );
$profile = $pathParts[1];
if ( $profile ) {
if ( '@' !== $profile[0] && $includeAt ) {
$profile = '@' . $profile;
}
if ( '@' === $profile[0] && ! $includeAt ) {
$profile = ltrim( $profile, '@' );
}
}
}
return $profile;
}
/**
* Get additional twitter data.
*
* @since 4.0.0
*
* @return array An array of additional twitter data.
*/
public function getAdditionalData() {
if ( ! aioseo()->options->social->twitter->general->additionalData ) {
return [];
}
$data = [];
$post = aioseo()->helpers->getPost();
if ( ! is_a( $post, 'WP_Post' ) ) {
return $data;
}
if ( $post->post_author && post_type_supports( $post->post_type, 'author' ) ) {
$data[] = [
'label' => __( 'Written by', 'all-in-one-seo-pack' ),
'value' => get_the_author_meta( 'display_name', $post->post_author )
];
}
if ( ! empty( $post->post_content ) ) {
$minutes = $this->getReadingTime( $post->post_content );
if ( ! empty( $minutes ) ) {
$data[] = [
'label' => __( 'Est. reading time', 'all-in-one-seo-pack' ),
// Translators: 1 - The estimated reading time.
'value' => sprintf( _n( '%1$s minute', '%1$s minutes', $minutes, 'all-in-one-seo-pack' ), $minutes )
];
}
}
return $data;
}
/**
* Returns the estimated reading time for a string.
*
* @since 4.0.0
*
* @param string $string The string to count.
* @return integer The estimated reading time as an integer.
*/
private function getReadingTime( $string ) {
$wpm = 200;
$word = str_word_count( wp_strip_all_tags( $string ) );
return round( $word / $wpm );
}
} Schema/Helpers.php 0000666 00000006776 15113050716 0010102 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Contains helper methods for our schema classes.
*
* @since 4.2.5
*/
class Helpers {
/**
* Checks whether the schema markup feature is enabled.
*
* @since 4.2.5
*
* @return bool Whether the schema markup feature is enabled or not.
*/
public function isEnabled() {
$isEnabled = ! in_array( 'enableSchemaMarkup', aioseo()->internalOptions->deprecatedOptions, true ) || aioseo()->options->deprecated->searchAppearance->global->schema->enableSchemaMarkup;
return ! apply_filters( 'aioseo_schema_disable', ! $isEnabled );
}
/**
* Strips HTML and removes all blank properties in each of our graphs.
* Also parses properties that might contain smart tags.
*
* @since 4.0.13
* @version 4.2.5
*
* @param array $data The graph data.
* @param string $parentKey The key of the group parent (optional).
* @param bool $replaceTags Whether the smart tags should be replaced.
* @return array The cleaned graph data.
*/
public function cleanAndParseData( $data, $parentKey = '', $replaceTags = true ) {
foreach ( $data as $k => &$v ) {
if ( is_numeric( $v ) || is_bool( $v ) || is_null( $v ) ) {
// Do nothing.
} elseif ( is_array( $v ) ) {
$v = $this->cleanAndParseData( $v, $k, $replaceTags );
} else {
// Check if the prop can contain some HTML tags.
if (
isset( aioseo()->schema->htmlAllowedFields[ $parentKey ] ) &&
in_array( $k, aioseo()->schema->htmlAllowedFields[ $parentKey ], true )
) {
$v = trim( wp_kses_post( $v ) );
} else {
$v = trim( wp_strip_all_tags( $v ) );
}
$v = $replaceTags ? aioseo()->tags->replaceTags( $v, get_the_ID() ) : $v;
}
if ( empty( $v ) && ! in_array( $k, aioseo()->schema->nullableFields, true ) ) {
unset( $data[ $k ] );
} else {
$data[ $k ] = $v;
}
}
return $data;
}
/**
* Sorts the schema data and then returns it as JSON.
* We temporarily change the floating point precision in order to prevent rounding errors.
* Otherwise e.g. 4.9 could be output as 4.90000004.
*
* @since 4.2.7
*
* @param array $schema The schema data.
* @param bool $replaceTags Whether the smart tags should be replaced.
* @return string The schema as JSON.
*/
public function getOutput( $schema, $replaceTags = true ) {
$schema['@graph'] = apply_filters( 'aioseo_schema_output', $schema['@graph'] );
$schema['@graph'] = $this->cleanAndParseData( $schema['@graph'], '', $replaceTags );
// Sort the graphs alphabetically.
usort( $schema['@graph'], function ( $a, $b ) {
$typeA = $a['@type'] ?? null;
$typeB = $b['@type'] ?? null;
if ( is_null( $typeA ) || is_array( $typeA ) ) {
return 1;
}
if ( is_null( $typeB ) || is_array( $typeB ) ) {
return -1;
}
return strcmp( $typeA, $typeB );
} );
// Allow users to control the default json_encode flags.
// Some users report better SEO performance when non-Latin unicode characters are not escaped.
$jsonFlags = apply_filters( 'aioseo_schema_json_flags', 0 );
$json = isset( $_GET['aioseo-dev'] ) || aioseo()->schema->generatingValidatorOutput // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
? aioseo()->helpers->wpJsonEncode( $schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
: aioseo()->helpers->wpJsonEncode( $schema, $jsonFlags );
return $json;
}
} Schema/Breadcrumb.php 0000666 00000023613 15113050716 0010533 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Determines the breadcrumb trail.
*
* @since 4.0.0
*/
class Breadcrumb {
/**
* Returns the breadcrumb trail for the homepage.
*
* @since 4.0.0
*
* @return array The breadcrumb trail.
*/
public function home() {
// Since we just need the root breadcrumb (homepage), we can call this immediately without passing any breadcrumbs.
return $this->setPositions();
}
/**
* Returns the breadcrumb trail for the requested post.
*
* @since 4.0.0
*
* @param \WP_Post $post The post object.
* @return array The breadcrumb trail.
*/
public function post( $post ) {
// Check if page is the static homepage.
if ( aioseo()->helpers->isStaticHomePage() ) {
return $this->home();
}
if ( is_post_type_hierarchical( $post->post_type ) ) {
return $this->setPositions( $this->postHierarchical( $post ) );
}
return $this->setPositions( $this->postNonHierarchical( $post ) );
}
/**
* Returns the breadcrumb trail for a hierarchical post.
*
* @since 4.0.0
*
* @param \WP_Post $post The post object.
* @return array The breadcrumb trail.
*/
private function postHierarchical( $post ) {
$breadcrumbs = [];
do {
array_unshift(
$breadcrumbs,
[
'name' => $post->post_title,
'description' => aioseo()->meta->description->getDescription( $post ),
'url' => get_permalink( $post ),
'type' => aioseo()->helpers->isWooCommerceShopPage( $post->ID ) || is_home() ? 'CollectionPage' : $this->getPostWebPageGraph()
]
);
if ( $post->post_parent ) {
$post = get_post( $post->post_parent );
} else {
$post = false;
}
} while ( $post );
return $breadcrumbs;
}
/**
* Returns the breadcrumb trail for a non-hierarchical post.
*
* In this case we need to compare the permalink structure with the permalink of the requested post and loop through all objects we're able to find.
*
* @since 4.0.0
*
* @param \WP_Post $post The post object.
* @return array The breadcrumb trail.
*/
private function postNonHierarchical( $post ) {
global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
$homeUrl = aioseo()->helpers->escapeRegex( home_url() );
$permalink = get_permalink();
$slug = preg_replace( "/$homeUrl/", '', (string) $permalink );
$tags = array_filter( explode( '/', get_option( 'permalink_structure' ) ) ); // Permalink structure exploded into separate tag strings.
$objects = array_filter( explode( '/', $slug ) ); // Permalink slug exploded into separate object slugs.
$postGraph = $this->getPostWebPageGraph();
if ( count( $tags ) !== count( $objects ) ) {
return [
'name' => $post->post_title,
'description' => aioseo()->meta->description->getDescription( $post ),
'url' => $permalink,
'type' => $postGraph
];
}
$pairs = array_reverse( array_combine( $tags, $objects ) );
$breadcrumbs = [];
$dateName = null;
$timestamp = strtotime( $post->post_date );
foreach ( $pairs as $tag => $object ) {
// Escape the delimiter.
$escObject = aioseo()->helpers->escapeRegex( $object );
// Determine the slug for the object.
preg_match( "/.*{$escObject}[\/]/", (string) $permalink, $url );
if ( empty( $url[0] ) ) {
continue;
}
$breadcrumb = [];
switch ( $tag ) {
case '%category%':
$term = aioseo()->standalone->primaryTerm->getPrimaryTerm( $post->ID, 'category' );
if ( ! $term ) {
$term = get_category_by_slug( $object );
}
if ( ! $term ) {
break;
}
// phpcs:disable Squiz.NamingConventions.ValidVariableName
$oldQueriedObject = $wp_query->queried_object;
$wp_query->queried_object = $term;
$wp_query->is_category = true;
$breadcrumb = [
'name' => $term->name,
'description' => aioseo()->meta->description->getDescription(),
'url' => get_term_link( $term ),
'type' => 'CollectionPage'
];
$wp_query->queried_object = $oldQueriedObject;
$wp_query->is_category = false;
// phpcs:enable Squiz.NamingConventions.ValidVariableName
break;
case '%author%':
$breadcrumb = [
'name' => get_the_author_meta( 'display_name', $post->post_author ),
'description' => aioseo()->meta->description->helpers->prepare( aioseo()->options->searchAppearance->archives->author->metaDescription ),
'url' => $url[0],
'type' => 'ProfilePage'
];
break;
case '%postid%':
case '%postname%':
$breadcrumb = [
'name' => $post->post_title,
'description' => aioseo()->meta->description->getDescription( $post ),
'url' => $url[0],
'type' => $postGraph
];
break;
case '%year%':
$dateName = gmdate( 'Y', $timestamp );
case '%monthnum%':
if ( ! $dateName ) {
$dateName = gmdate( 'F', $timestamp );
}
case '%day%':
if ( ! $dateName ) {
$dateName = gmdate( 'j', $timestamp );
}
$breadcrumb = [
'name' => $dateName,
'description' => aioseo()->meta->description->helpers->prepare( aioseo()->options->searchAppearance->archives->date->metaDescription ),
'url' => $url[0],
'type' => 'CollectionPage'
];
$dateName = null;
break;
default:
break;
}
if ( $breadcrumb ) {
array_unshift( $breadcrumbs, $breadcrumb );
}
}
return $breadcrumbs;
}
/**
* Returns the breadcrumb trail for the requested term.
*
* @since 4.0.0
*
* @param \WP_Term $term The term object.
* @return array The breadcrumb trail.
*/
public function term( $term ) {
if ( 'product_attributes' === $term->taxonomy ) {
$term = get_term( $term->term_id );
}
$breadcrumbs = [];
do {
array_unshift(
$breadcrumbs,
[
'name' => $term->name,
'description' => aioseo()->meta->description->getDescription(),
'url' => get_term_link( $term, $term->taxonomy ),
'type' => 'CollectionPage'
]
);
if ( $term->parent ) {
$term = aioseo()->helpers->getTerm( $term->parent, $term->taxonomy );
} else {
$term = false;
}
} while ( $term );
return $this->setPositions( $breadcrumbs );
}
/**
* Returns the breadcrumb trail for the requested date archive.
*
* @since 4.0.0
*
* @return array The breadcrumb trail.
*/
public function date() {
// phpcs:disable Squiz.NamingConventions.ValidVariableName
global $wp_query;
$oldYear = $wp_query->is_year;
$oldMonth = $wp_query->is_month;
$oldDay = $wp_query->is_day;
$wp_query->is_year = true;
$wp_query->is_month = false;
$wp_query->is_day = false;
$breadcrumbs = [
[
'name' => get_the_date( 'Y' ),
'description' => aioseo()->meta->description->getDescription(),
'url' => trailingslashit( get_year_link( $wp_query->query_vars['year'] ) ),
'type' => 'CollectionPage'
]
];
$wp_query->is_year = $oldYear;
// Fall through if data archive is more specific than the year.
if ( is_year() ) {
return $this->setPositions( $breadcrumbs );
}
$wp_query->is_month = true;
$breadcrumbs[] = [
'name' => get_the_date( 'F, Y' ),
'description' => aioseo()->meta->description->getDescription(),
'url' => trailingslashit( get_month_link(
$wp_query->query_vars['year'],
$wp_query->query_vars['monthnum']
) ),
'type' => 'CollectionPage'
];
$wp_query->is_month = $oldMonth;
// Fall through if data archive is more specific than the year & month.
if ( is_month() ) {
return $this->setPositions( $breadcrumbs );
}
$wp_query->is_day = $oldDay;
$breadcrumbs[] = [
'name' => get_the_date(),
'description' => aioseo()->meta->description->getDescription(),
'url' => trailingslashit( get_day_link(
$wp_query->query_vars['year'],
$wp_query->query_vars['monthnum'],
$wp_query->query_vars['day']
) ),
'type' => 'CollectionPage'
];
// phpcs:enable Squiz.NamingConventions.ValidVariableName
return $this->setPositions( $breadcrumbs );
}
/**
* Sets the position for each breadcrumb after adding the root breadcrumb first.
*
* If no breadcrumbs are passed, then we assume we're on the homepage and just need the root breadcrumb.
*
* @since 4.0.0
*
* @param array $breadcrumbs The breadcrumb trail.
* @return array The modified breadcrumb trail.
*/
public function setPositions( $breadcrumbs = [] ) {
// If the array isn't two-dimensional, then we need to wrap it in another array before continuing.
if (
count( $breadcrumbs ) &&
count( $breadcrumbs ) === count( $breadcrumbs, COUNT_RECURSIVE )
) {
$breadcrumbs = [ $breadcrumbs ];
}
// The homepage needs to be root item of all trails.
$homepage = [
// Translators: This refers to the homepage of the site.
'name' => apply_filters( 'aioseo_schema_breadcrumbs_home', __( 'Home', 'all-in-one-seo-pack' ) ),
'description' => aioseo()->meta->description->getHomePageDescription(),
'url' => trailingslashit( home_url() ),
'type' => 'posts' === get_option( 'show_on_front' ) ? 'CollectionPage' : 'WebPage'
];
array_unshift( $breadcrumbs, $homepage );
$breadcrumbs = array_filter( $breadcrumbs );
foreach ( $breadcrumbs as $index => &$breadcrumb ) {
$breadcrumb['position'] = $index + 1;
}
return $breadcrumbs;
}
/**
* Returns the most relevant WebPage graph for the post.
*
* @since 4.2.5
*
* @return string The graph name.
*/
private function getPostWebPageGraph() {
foreach ( aioseo()->schema->graphs as $graphName ) {
if ( in_array( $graphName, aioseo()->schema->webPageGraphs, true ) ) {
return $graphName;
}
}
// Return the default if no WebPage graph was found.
return 'WebPage';
}
} Schema/Schema.php 0000666 00000021213 15113050716 0007657 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
use AIOSEO\Plugin\Common\Integrations\BbPress as BbPressIntegration;
/**
* Builds our schema.
*
* @since 4.0.0
*/
class Schema {
/**
* The graphs that need to be generated.
*
* @since 4.2.5
*
* @var array
*/
public $graphs = [];
/**
* The context data.
*
* @since 4.0.0
*
* @var array
*/
public $context = [];
/**
* Helpers class instance.
*
* @since 4.2.7
*
* @var Helpers
*/
public $helpers = null;
/**
* The subdirectories that contain graph classes.
*
* @since 4.2.5
*
* @var array
*/
protected $graphSubDirectories = [
'Article',
'KnowledgeGraph',
'WebPage'
];
/**
* All existing WebPage graphs.
*
* @since 4.0.0
*
* @var array
*/
public $webPageGraphs = [
'WebPage',
'AboutPage',
'CheckoutPage',
'CollectionPage',
'ContactPage',
'FAQPage',
'ItemPage',
'MedicalWebPage',
'ProfilePage',
'RealEstateListing',
'SearchResultsPage'
];
/**
* Fields that can be 0 or null, which shouldn't be stripped when cleaning the data.
*
* @since 4.1.2
*
* @var array
*/
public $nullableFields = [
'price', // Needs to be 0 if free for Software Application.
'ratingValue', // Needs to be 0 for 0 star ratings.
'value', // Needs to be 0 if free for product shipping details.
'minValue', // Needs to be 0 for product delivery time.
'maxValue', // Needs to be 0 for product delivery time.
'suggestedMinAge' // Needs to be 0 for PeopleAudience minimum age.
];
/**
* List of mapped parents with properties that are allowed to contain a restricted set of HTML tags.
*
* @since 4.2.3
*
* @var array
*/
public $htmlAllowedFields = [
// FAQPage
'acceptedAnswer' => [
'text'
]
];
/**
* Whether we are generating the validator output.
*
* @since 4.6.3
*
* @var bool
*/
public $generatingValidatorOutput = false;
/**
* Class constructor.
*/
public function __construct() {
// No AJAX check since we need to be able to grab the schema output via the REST API.
if ( wp_doing_cron() ) {
return;
}
$this->helpers = new Helpers();
}
/**
* Returns the JSON schema output.
*
* @since 4.0.0
*
* @return string The JSON schema output.
*/
public function get() {
// First, check if the schema is disabled.
if ( ! $this->helpers->isEnabled() ) {
return '';
}
$this->determineSmartGraphsAndContext();
return $this->generateSchema();
}
/**
* Generates the JSON schema after the graphs/context have been determined.
*
* @since 4.2.5
*
* @return string The JSON schema output.
*/
protected function generateSchema() {
// Now, filter the graphs.
$this->graphs = apply_filters(
'aioseo_schema_graphs',
array_unique( array_filter( array_values( $this->graphs ) ) )
);
if ( ! $this->graphs ) {
return '';
}
// Check if a WebPage graph is included. Otherwise add the default one.
$webPageGraphFound = false;
foreach ( $this->graphs as $graphName ) {
if ( in_array( $graphName, $this->webPageGraphs, true ) ) {
$webPageGraphFound = true;
break;
}
}
if ( ! $webPageGraphFound ) {
$this->graphs[] = 'WebPage';
}
// Now that we've determined the graphs, start generating their data.
$schema = [
'@context' => 'https://schema.org',
'@graph' => []
];
// By determining the length of the array after every iteration, we are able to add additional graphs during runtime.
// e.g. The Article graph may require a Person graph to be output for the author.
$this->graphs = array_values( $this->graphs );
for ( $i = 0; $i < count( $this->graphs ); $i++ ) {
$namespace = $this->getGraphNamespace( $this->graphs[ $i ] );
if ( $namespace ) {
$schema['@graph'][] = ( new $namespace() )->get();
}
}
return aioseo()->schema->helpers->getOutput( $schema );
}
/**
* Gets the relevant namespace for the given graph.
*
* @since 4.2.5
*
* @param string $graphName The graph name.
* @return string The namespace.
*/
protected function getGraphNamespace( $graphName ) {
$namespace = "\AIOSEO\Plugin\Common\Schema\Graphs\\{$graphName}";
if ( class_exists( $namespace ) ) {
return $namespace;
}
// If we can't find it in the root dir, check if we can find it in a sub dir.
foreach ( $this->graphSubDirectories as $dirName ) {
$namespace = "\AIOSEO\Plugin\Common\Schema\Graphs\\{$dirName}\\{$graphName}";
if ( class_exists( $namespace ) ) {
return $namespace;
}
}
return '';
}
/**
* Determines the smart graphs that need to be output by default, as well as the current context for the breadcrumbs.
*
* @since 4.2.5
*
* @return void
*/
protected function determineSmartGraphsAndContext() {
$this->graphs = array_merge( $this->graphs, $this->getDefaultGraphs() );
$contextInstance = new Context();
$this->context = $contextInstance->defaults();
if ( BuddyPressIntegration::isComponentPage() ) {
aioseo()->standalone->buddyPress->component->determineSchemaGraphsAndContext( $contextInstance );
return;
}
if ( BbPressIntegration::isComponentPage() ) {
aioseo()->standalone->bbPress->component->determineSchemaGraphsAndContext();
return;
}
if ( aioseo()->helpers->isDynamicHomePage() ) {
$this->graphs[] = 'CollectionPage';
$this->context = $contextInstance->home();
return;
}
if ( is_home() || aioseo()->helpers->isWooCommerceShopPage() ) {
$this->graphs[] = 'CollectionPage';
$this->context = $contextInstance->post();
return;
}
if ( is_singular() ) {
$this->determineContextSingular( $contextInstance );
if ( is_singular( 'web-story' ) ) {
$this->graphs[] = 'AmpStory';
}
}
if ( is_category() || is_tag() || is_tax() ) {
$this->graphs[] = 'CollectionPage';
$this->context = $contextInstance->term();
return;
}
if ( is_author() ) {
$this->graphs[] = 'ProfilePage';
$this->graphs[] = 'PersonAuthor';
$this->context = $contextInstance->author();
}
if ( is_post_type_archive() ) {
$this->graphs[] = 'CollectionPage';
$this->context = $contextInstance->postArchive();
return;
}
if ( is_date() ) {
$this->graphs[] = 'CollectionPage';
$this->context = $contextInstance->date();
return;
}
if ( is_search() ) {
$this->graphs[] = 'SearchResultsPage';
$this->context = $contextInstance->search();
return;
}
if ( is_404() ) {
$this->context = $contextInstance->notFound();
}
}
/**
* Determines the smart graphs and context for singular pages.
*
* @since 4.2.6
*
* @param Context $contextInstance The Context class instance.
* @return void
*/
protected function determineContextSingular( $contextInstance ) {
// If the current request is for the validator, we can't include the default graph here.
// We need to include the default graph that the validator sent.
// Don't do this if we're in Pro since we then need to get it from the post meta.
if ( ! $this->generatingValidatorOutput ) {
$this->graphs[] = $this->getDefaultPostGraph();
}
$this->context = $contextInstance->post();
}
/**
* Returns the default graph for the post type.
*
* @since 4.2.6
*
* @return string The default graph.
*/
public function getDefaultPostGraph() {
return $this->getDefaultPostTypeGraph();
}
/**
* Returns the default graph for the current post type.
*
* @since 4.2.5
*
* @param \WP_Post $post The post object.
* @return string The default graph.
*/
public function getDefaultPostTypeGraph( $post = null ) {
$post = $post ? $post : aioseo()->helpers->getPost();
if ( ! is_a( $post, 'WP_Post' ) ) {
return '';
}
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
if ( ! $dynamicOptions->searchAppearance->postTypes->has( $post->post_type ) ) {
return '';
}
$defaultType = $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->schemaType;
switch ( $defaultType ) {
case 'Article':
return $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->articleType;
case 'WebPage':
return $dynamicOptions->searchAppearance->postTypes->{$post->post_type}->webPageType;
default:
return $defaultType;
}
}
/**
* Returns the default graphs that should be output on every page, regardless of its type.
*
* @since 4.2.5
*
* @return array The default graphs.
*/
protected function getDefaultGraphs() {
$siteRepresents = ucfirst( aioseo()->options->searchAppearance->global->schema->siteRepresents );
return [
'BreadcrumbList',
'Kg' . $siteRepresents,
'WebSite'
];
}
} Schema/Context.php 0000666 00000014327 15113050716 0010113 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Determines the context.
*
* @since 4.0.0
*/
class Context {
/**
* Breadcrumb class instance.
*
* @since 4.2.7
*
* @var Breadcrumb
*/
public $breadcrumb = null;
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
$this->breadcrumb = new Breadcrumb();
}
/**
* Returns the default context data.
*
* @since 4.3.0
*
* @return array The context data.
*/
public function defaults() {
return [
'name' => aioseo()->meta->title->getTitle(),
'description' => aioseo()->meta->description->getDescription(),
'url' => aioseo()->helpers->getUrl(),
'breadcrumb' => []
];
}
/**
* Returns the context data for the homepage.
*
* @since 4.0.0
*
* @return array $context The context data.
*/
public function home() {
$context = [
'url' => aioseo()->helpers->getUrl(),
'breadcrumb' => $this->breadcrumb->home(),
'name' => aioseo()->meta->title->getTitle(),
'description' => aioseo()->meta->description->getDescription()
];
// Homepage set to show latest posts.
if ( 'posts' === get_option( 'show_on_front' ) && is_home() ) {
return $context;
}
// Homepage set to static page.
$post = aioseo()->helpers->getPost();
if ( ! $post ) {
return [
'name' => '',
'description' => '',
'url' => aioseo()->helpers->getUrl(),
'breadcrumb' => [],
];
}
$context['object'] = $post;
return $context;
}
/**
* Returns the context data for the requested post.
*
* @since 4.0.0
*
* @return array The context data.
*/
public function post() {
$post = aioseo()->helpers->getPost();
if ( ! $post ) {
return [
'name' => '',
'description' => '',
'url' => aioseo()->helpers->getUrl(),
'breadcrumb' => [],
];
}
return [
'name' => aioseo()->meta->title->getTitle( $post ),
'description' => aioseo()->meta->description->getDescription( $post ),
'url' => aioseo()->helpers->getUrl(),
'breadcrumb' => $this->breadcrumb->post( $post ),
'object' => $post,
];
}
/**
* Returns the context data for the requested term archive.
*
* @since 4.0.0
*
* @return array The context data.
*/
public function term() {
$term = aioseo()->helpers->getTerm();
if ( ! $term ) {
return [
'name' => '',
'description' => '',
'url' => aioseo()->helpers->getUrl(),
'breadcrumb' => [],
];
}
return [
'name' => aioseo()->meta->title->getTitle(),
'description' => aioseo()->meta->description->getDescription(),
'url' => aioseo()->helpers->getUrl(),
'breadcrumb' => $this->breadcrumb->term( $term )
];
}
/**
* Returns the context data for the requested author archive.
*
* @since 4.0.0
*
* @return array The context data.
*/
public function author() {
$author = get_queried_object();
if ( ! $author ) {
return [
'name' => '',
'description' => '',
'url' => aioseo()->helpers->getUrl(),
'breadcrumb' => [],
];
}
$title = aioseo()->meta->title->getTitle();
$description = aioseo()->meta->description->getDescription();
$url = aioseo()->helpers->getUrl();
if ( ! $description ) {
$description = get_the_author_meta( 'description', $author->ID );
}
return [
'name' => $title,
'description' => $description,
'url' => $url,
'breadcrumb' => $this->breadcrumb->setPositions( [
'name' => get_the_author_meta( 'display_name', $author->ID ),
'description' => $description,
'url' => $url,
'type' => 'CollectionPage'
] )
];
}
/**
* Returns the context data for the requested post archive.
*
* @since 4.0.0
*
* @return array The context data.
*/
public function postArchive() {
$postType = get_queried_object();
if ( ! $postType ) {
return [
'name' => '',
'description' => '',
'url' => aioseo()->helpers->getUrl(),
'breadcrumb' => [],
];
}
$title = aioseo()->meta->title->getTitle();
$description = aioseo()->meta->description->getDescription();
$url = aioseo()->helpers->getUrl();
return [
'name' => $title,
'description' => $description,
'url' => $url,
'breadcrumb' => $this->breadcrumb->setPositions( [
'name' => $postType->label,
'description' => $description,
'url' => $url,
'type' => 'CollectionPage'
] )
];
}
/**
* Returns the context data for the requested data archive.
*
* @since 4.0.0
*
* @return array $context The context data.
*/
public function date() {
$context = [
'name' => aioseo()->meta->title->getTitle(),
'description' => aioseo()->meta->description->getDescription(),
'url' => aioseo()->helpers->getUrl()
];
$context['breadcrumb'] = $this->breadcrumb->date();
return $context;
}
/**
* Returns the context data for the search page.
*
* @since 4.0.0
*
* @return array The context data.
*/
public function search() {
global $s;
$title = aioseo()->meta->title->getTitle();
$description = aioseo()->meta->description->getDescription();
$url = aioseo()->helpers->getUrl();
return [
'name' => $title,
'description' => $description,
'url' => $url,
'breadcrumb' => $this->breadcrumb->setPositions( [
'name' => $s ? $s : $title,
'description' => $description,
'url' => $url,
'type' => 'SearchResultsPage'
] )
];
}
/**
* Returns the context data for the 404 Not Found page.
*
* @since 4.0.0
*
* @return array The context data.
*/
public function notFound() {
$title = aioseo()->meta->title->getTitle();
$description = aioseo()->meta->description->getDescription();
$url = aioseo()->helpers->getUrl();
return [
'name' => $title,
'description' => $description,
'url' => $url,
'breadcrumb' => $this->breadcrumb->setPositions( [
'name' => __( 'Not Found', 'all-in-one-seo-pack' ),
'description' => $description,
'url' => $url
] )
];
}
} Schema/Graphs/WebSite.php 0000666 00000001747 15113050716 0011257 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WebSite graph class.
*
* @since 4.0.0
*/
class WebSite extends Graph {
/**
* Returns the graph data.
*
* @since 4.0.0
*
* @return array $data The graph data.
*/
public function get() {
$homeUrl = trailingslashit( home_url() );
$data = [
'@type' => 'WebSite',
'@id' => $homeUrl . '#website',
'url' => $homeUrl,
'name' => aioseo()->helpers->getWebsiteName(),
'alternateName' => aioseo()->tags->replaceTags( aioseo()->options->searchAppearance->global->schema->websiteAlternateName ),
'description' => aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) ),
'inLanguage' => aioseo()->helpers->currentLanguageCodeBCP47(),
'publisher' => [ '@id' => $homeUrl . '#' . aioseo()->options->searchAppearance->global->schema->siteRepresents ]
];
return $data;
}
} Schema/Graphs/AmpStory.php 0000666 00000002471 15113050716 0011466 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* AmpStory graph class.
*
* @since 4.7.6
*/
class AmpStory extends Graph {
/**
* Returns the graph data.
*
* @since 4.7.6
*
* @return array The parsed graph data.
*/
public function get() {
$post = aioseo()->helpers->getPost();
if ( ! is_a( $post, 'WP_Post' ) || 'web-story' !== $post->post_type ) {
return [];
}
$data = [
'@type' => 'AmpStory',
'@id' => aioseo()->schema->context['url'] . '#amp-story',
'name' => aioseo()->schema->context['name'],
'headline' => get_the_title(),
'author' => [
'@id' => get_author_posts_url( $post->post_author ) . '#author'
],
'publisher' => [ '@id' => trailingslashit( home_url() ) . '#' . aioseo()->options->searchAppearance->global->schema->siteRepresents ],
'image' => $this->getFeaturedImage(),
'datePublished' => mysql2date( DATE_W3C, $post->post_date, false ),
'dateModified' => mysql2date( DATE_W3C, $post->post_modified, false ),
'inLanguage' => aioseo()->helpers->currentLanguageCodeBCP47()
];
if ( ! in_array( 'PersonAuthor', aioseo()->schema->graphs, true ) ) {
aioseo()->schema->graphs[] = 'PersonAuthor';
}
return $data;
}
} Schema/Graphs/WebPage/ProfilePage.php 0000666 00000004274 15113050716 0013422 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
/**
* ProfilePage graph class.
*
* @since 4.0.0
*/
class ProfilePage extends WebPage {
/**
* The graph type.
*
* @since 4.5.6
*
* @var string
*/
protected $type = 'ProfilePage';
/**
* Returns the graph data.
*
* @since 4.5.4
*
* @return array The graph data.
*/
public function get() {
$data = parent::get();
$post = aioseo()->helpers->getPost();
$author = get_queried_object();
if (
! is_a( $author, 'WP_User' ) &&
( is_singular() && ! is_a( $post, 'WP_Post' ) )
) {
return [];
}
global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
$articles = [];
$authorId = $author->ID ?? $post->post_author ?? 0;
foreach ( $wp_query->posts as $post ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
if ( $post->post_author !== $authorId ) {
continue;
}
$articles[] = [
'@type' => 'Article',
'url' => get_permalink( $post->ID ),
'headline' => $post->post_title,
'datePublished' => mysql2date( DATE_W3C, $post->post_date, false ),
'dateModified' => mysql2date( DATE_W3C, $post->post_modified, false ),
'author' => [
'@id' => get_author_posts_url( $authorId ) . '#author'
]
];
}
$data = array_merge( $data, [
'dateCreated' => mysql2date( DATE_W3C, $author->user_registered, false ),
'mainEntity' => [
'@id' => get_author_posts_url( $authorId ) . '#author'
],
'hasPart' => $articles
] );
if (
BuddyPressIntegration::isComponentPage() &&
'bp-member_single' === aioseo()->standalone->buddyPress->component->templateType
) {
if ( ! isset( $data['mainEntity'] ) ) {
$data['mainEntity'] = [];
}
$data['mainEntity']['@type'] = 'Person';
$data['mainEntity']['name'] = aioseo()->standalone->buddyPress->component->author->display_name;
$data['mainEntity']['url'] = BuddyPressIntegration::getComponentSingleUrl( 'member', aioseo()->standalone->buddyPress->component->author->ID );
}
return $data;
}
} Schema/Graphs/WebPage/CheckoutPage.php 0000666 00000000642 15113050716 0013562 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* CheckoutPage graph class.
*
* @since 4.6.4
*/
class CheckoutPage extends WebPage {
/**
* The graph type.
*
* This value can be overridden by WebPage child graphs that are more specific.
*
* @since 4.6.4
*
* @var string
*/
protected $type = 'CheckoutPage';
} Schema/Graphs/WebPage/MedicalWebPage.php 0000666 00000000650 15113050716 0014010 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* MedicalWebPage graph class.
*
* @since 4.6.4
*/
class MedicalWebPage extends WebPage {
/**
* The graph type.
*
* This value can be overridden by WebPage child graphs that are more specific.
*
* @since 4.6.4
*
* @var string
*/
protected $type = 'MedicalWebPage';
} Schema/Graphs/WebPage/AboutPage.php 0000666 00000000504 15113050716 0013064 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* AboutPage graph class.
*
* @since 4.0.0
*/
class AboutPage extends WebPage {
/**
* The graph type.
*
* @since 4.0.0
*
* @var string
*/
protected $type = 'AboutPage';
} Schema/Graphs/WebPage/CollectionPage.php 0000666 00000000523 15113050716 0014106 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* CollectionPage graph class.
*
* @since 4.0.0
*/
class CollectionPage extends WebPage {
/**
* The graph type.
*
* @since 4.0.0
*
* @var string
*/
protected $type = 'CollectionPage';
} Schema/Graphs/WebPage/FAQPage.php 0000666 00000000476 15113050716 0012431 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* FAQPage graph class.
*
* @since 4.0.0
*/
class FAQPage extends WebPage {
/**
* The graph type.
*
* @since 4.0.0
*
* @var string
*/
protected $type = 'FAQPage';
} Schema/Graphs/WebPage/ItemPage.php 0000666 00000000626 15113050716 0012715 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* ItemPage graph class.
*
* @since 4.0.0
*/
class ItemPage extends WebPage {
/**
* The graph type.
*
* This value can be overridden by WebPage child graphs that are more specific.
*
* @since 4.0.0
*
* @var string
*/
protected $type = 'ItemPage';
} Schema/Graphs/WebPage/PersonAuthor.php 0000666 00000004016 15113050716 0013650 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Schema\Graphs;
/**
* Person Author graph class.
* This a secondary Person graph for post authors and BuddyPress profile pages.
*
* @since 4.0.0
*/
class PersonAuthor extends Graphs\Graph {
/**
* Returns the graph data.
*
* @since 4.0.0
*
* @param int $userId The user ID.
* @return array $data The graph data.
*/
public function get( $userId = null ) {
$post = aioseo()->helpers->getPost();
$user = get_queried_object();
$isAuthorPage = is_author() && is_a( $user, 'WP_User' );
if (
(
( ! is_singular() && ! $isAuthorPage ) ||
( is_singular() && ! is_a( $post, 'WP_Post' ) )
) &&
! $userId
) {
return [];
}
// Dynamically determine the User ID.
if ( ! $userId ) {
$userId = $isAuthorPage ? $user->ID : $post->post_author;
if ( function_exists( 'bp_is_user' ) && bp_is_user() ) {
$userId = intval( wp_get_current_user()->ID );
}
}
if ( ! $userId ) {
return [];
}
$authorUrl = get_author_posts_url( $userId );
$data = [
'@type' => 'Person',
'@id' => $authorUrl . '#author',
'url' => $authorUrl,
'name' => get_the_author_meta( 'display_name', $userId )
];
$avatar = $this->avatar( $userId, 'authorImage' );
if ( $avatar ) {
$data['image'] = $avatar;
}
$socialUrls = array_values( $this->getUserProfiles( $userId ) );
if ( $socialUrls ) {
$data['sameAs'] = $socialUrls;
}
if ( is_author() ) {
$data['mainEntityOfPage'] = [
'@id' => aioseo()->schema->context['url'] . '#profilepage'
];
}
// Check if our addons need to modify this graph.
$addonsPersonAuthorData = array_filter( aioseo()->addons->doAddonFunction( 'personAuthor', 'get', [
'userId' => $userId,
'data' => $data
] ) );
foreach ( $addonsPersonAuthorData as $addonPersonAuthorData ) {
$data = array_merge( $data, $addonPersonAuthorData );
}
return $data;
}
} Schema/Graphs/WebPage/SearchResultsPage.php 0000666 00000000534 15113050716 0014604 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* SearchResultsPage graph class.
*
* @since 4.0.0
*/
class SearchResultsPage extends WebPage {
/**
* The graph type.
*
* @since 4.0.0
*
* @var string
*/
protected $type = 'SearchResultsPage';
} Schema/Graphs/WebPage/WebPage.php 0000666 00000005577 15113050716 0012546 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Schema\Graphs;
/**
* WebPage graph class.
*
* @since 4.0.0
*/
class WebPage extends Graphs\Graph {
/**
* The graph type.
*
* This value can be overridden by WebPage child graphs that are more specific.
*
* @since 4.0.0
*
* @var string
*/
protected $type = 'WebPage';
/**
* Returns the graph data.
*
* @since 4.0.0
*
* @return array $data The graph data.
*/
public function get() {
$homeUrl = trailingslashit( home_url() );
$data = [
'@type' => $this->type,
'@id' => aioseo()->schema->context['url'] . '#' . strtolower( $this->type ),
'url' => aioseo()->schema->context['url'],
'name' => aioseo()->meta->title->getTitle(),
'description' => aioseo()->schema->context['description'],
'inLanguage' => aioseo()->helpers->currentLanguageCodeBCP47(),
'isPartOf' => [ '@id' => $homeUrl . '#website' ]
];
$breadcrumbs = aioseo()->breadcrumbs->frontend->getBreadcrumbs() ?? '';
if ( ! empty( $breadcrumbs ) ) {
$data['breadcrumb'] = [ '@id' => aioseo()->schema->context['url'] . '#breadcrumblist' ];
}
if ( is_singular() && 'page' !== get_post_type() ) {
$post = aioseo()->helpers->getPost();
if ( is_a( $post, 'WP_Post' ) && post_type_supports( $post->post_type, 'author' ) ) {
$author = get_author_posts_url( $post->post_author );
if ( ! empty( $author ) ) {
if ( ! in_array( 'PersonAuthor', aioseo()->schema->graphs, true ) ) {
aioseo()->schema->graphs[] = 'PersonAuthor';
}
$data['author'] = [ '@id' => $author . '#author' ];
$data['creator'] = [ '@id' => $author . '#author' ];
}
}
}
if ( isset( aioseo()->schema->context['description'] ) && aioseo()->schema->context['description'] ) {
$data['description'] = aioseo()->schema->context['description'];
}
if ( is_singular() ) {
if ( ! isset( aioseo()->schema->context['object'] ) || ! aioseo()->schema->context['object'] ) {
return $this->getAddonData( $data, 'webPage' );
}
$post = aioseo()->schema->context['object'];
if ( has_post_thumbnail( $post ) ) {
$image = $this->image( get_post_thumbnail_id(), 'mainImage' );
if ( $image ) {
$data['image'] = $image;
$data['primaryImageOfPage'] = [
'@id' => aioseo()->schema->context['url'] . '#mainImage'
];
}
}
$data['datePublished'] = mysql2date( DATE_W3C, $post->post_date, false );
$data['dateModified'] = mysql2date( DATE_W3C, $post->post_modified, false );
return $this->getAddonData( $data, 'webPage' );
}
if ( is_front_page() ) {
$data['about'] = [ '@id' => trailingslashit( home_url() ) . '#' . aioseo()->options->searchAppearance->global->schema->siteRepresents ];
}
return $this->getAddonData( $data, 'webPage' );
}
} Schema/Graphs/WebPage/RealEstateListing.php 0000666 00000001244 15113050716 0014602 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* RealEstateListing graph class.
*
* @since 4.0.0
*/
class RealEstateListing extends WebPage {
/**
* The graph type.
*
* @since 4.0.0
*
* @var string
*/
protected $type = 'RealEstateListing';
/**
* Returns the graph data.
*
* @since 4.0.0
*
* @return array $data The graph data.
*/
public function get() {
$data = parent::get();
$post = aioseo()->helpers->getPost();
if ( ! $post ) {
return $data;
}
$data['datePosted'] = mysql2date( DATE_W3C, $post->post_date, false );
return $data;
}
} Schema/Graphs/WebPage/ContactPage.php 0000666 00000000512 15113050716 0013404 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\WebPage;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* ContactPage graph class.
*
* @since 4.0.0
*/
class ContactPage extends WebPage {
/**
* The graph type.
*
* @since 4.0.0
*
* @var string
*/
protected $type = 'ContactPage';
} Schema/Graphs/BreadcrumbList.php 0000666 00000004205 15113050716 0012607 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* BreadcrumbList graph class.
*
* @since 4.0.0
*/
class BreadcrumbList extends Graph {
/**
* Returns the graph data.
*
* @since 4.0.0
*
* @return array The graph data.
*/
public function get() {
$breadcrumbs = aioseo()->breadcrumbs->frontend->getBreadcrumbs() ?? '';
if ( ! $breadcrumbs ) {
return [];
}
// Set the position for each breadcrumb.
foreach ( $breadcrumbs as $k => $breadcrumb ) {
if ( ! isset( $breadcrumb['position'] ) ) {
$breadcrumbs[ $k ]['position'] = $k + 1;
}
}
$trailLength = count( $breadcrumbs );
if ( ! $trailLength ) {
return [];
}
$listItems = [];
foreach ( $breadcrumbs as $breadcrumb ) {
if ( empty( $breadcrumb['link'] ) ) {
continue;
}
$listItem = [
'@type' => 'ListItem',
'@id' => $breadcrumb['link'] . '#listItem',
'position' => $breadcrumb['position'],
'name' => $breadcrumb['label'] ?? ''
];
// Don't add "item" prop for last crumb.
if ( $trailLength !== $breadcrumb['position'] ) {
$listItem['item'] = $breadcrumb['link'];
}
if ( 1 === $trailLength ) {
$listItems[] = $listItem;
continue;
}
if ( $trailLength > $breadcrumb['position'] && ! empty( $breadcrumbs[ $breadcrumb['position'] ]['label'] ) ) {
$listItem['nextItem'] = [
'@type' => 'ListItem',
'@id' => $breadcrumbs[ $breadcrumb['position'] ]['link'] . '#listItem',
'name' => $breadcrumbs[ $breadcrumb['position'] ]['label'],
];
}
if ( 1 < $breadcrumb['position'] && ! empty( $breadcrumbs[ $breadcrumb['position'] - 2 ]['label'] ) ) {
$listItem['previousItem'] = [
'@type' => 'ListItem',
'@id' => $breadcrumbs[ $breadcrumb['position'] - 2 ]['link'] . '#listItem',
'name' => $breadcrumbs[ $breadcrumb['position'] - 2 ]['label'],
];
}
$listItems[] = $listItem;
}
$data = [
'@type' => 'BreadcrumbList',
'@id' => aioseo()->schema->context['url'] . '#breadcrumblist',
'itemListElement' => $listItems
];
return $data;
}
} Schema/Graphs/KnowledgeGraph/KgPerson.php 0000666 00000003457 15113050716 0014346 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\KnowledgeGraph;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use \AIOSEO\Plugin\Common\Schema\Graphs;
/**
* Knowledge Graph Person graph class.
* This is the main Person graph that can be set to represent the site.
*
* @since 4.0.0
*/
class KgPerson extends Graphs\Graph {
/**
* Returns the graph data.
*
* @since 4.0.0
*
* @return array $data The graph data.
*/
public function get() {
if ( 'person' !== aioseo()->options->searchAppearance->global->schema->siteRepresents ) {
return [];
}
$person = aioseo()->options->searchAppearance->global->schema->person;
if ( 'manual' === $person ) {
return $this->manual();
}
$person = intval( $person );
if ( empty( $person ) ) {
return [];
}
$data = [
'@type' => 'Person',
'@id' => trailingslashit( home_url() ) . '#person',
'name' => get_the_author_meta( 'display_name', $person )
];
$avatar = $this->avatar( $person, 'personImage' );
if ( $avatar ) {
$data['image'] = $avatar;
}
$socialUrls = array_values( $this->getUserProfiles( $person ) );
if ( $socialUrls ) {
$data['sameAs'] = $socialUrls;
}
return $data;
}
/**
* Returns the data for the person if it is set manually.
*
* @since 4.0.0
*
* @return array $data The graph data.
*/
private function manual() {
$data = [
'@type' => 'Person',
'@id' => trailingslashit( home_url() ) . '#person',
'name' => aioseo()->options->searchAppearance->global->schema->personName
];
$logo = aioseo()->options->searchAppearance->global->schema->personLogo;
if ( $logo ) {
$data['image'] = $logo;
}
$socialUrls = array_values( $this->getOrganizationProfiles() );
if ( $socialUrls ) {
$data['sameAs'] = $socialUrls;
}
return $data;
}
} Schema/Graphs/KnowledgeGraph/KgOrganization.php 0000666 00000005336 15113050716 0015542 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\KnowledgeGraph;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use \AIOSEO\Plugin\Common\Schema\Graphs;
/**
* Knowledge Graph Organization graph class.
*
* @since 4.0.0
*/
class KgOrganization extends Graphs\Graph {
/**
* Returns the graph data.
*
* @since 4.0.0
*
* @return array $data The graph data.
*/
public function get() {
$homeUrl = trailingslashit( home_url() );
$organizationName = aioseo()->tags->replaceTags( aioseo()->options->searchAppearance->global->schema->organizationName );
$organizationDescription = aioseo()->tags->replaceTags( aioseo()->options->searchAppearance->global->schema->organizationDescription );
$data = [
'@type' => 'Organization',
'@id' => $homeUrl . '#organization',
'name' => $organizationName ? $organizationName : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) ),
'description' => $organizationDescription,
'url' => $homeUrl,
'email' => aioseo()->options->searchAppearance->global->schema->email,
'telephone' => aioseo()->options->searchAppearance->global->schema->phone,
'foundingDate' => aioseo()->options->searchAppearance->global->schema->foundingDate
];
$numberOfEmployeesData = aioseo()->options->searchAppearance->global->schema->numberOfEmployees->all();
if (
$numberOfEmployeesData['isRange'] &&
isset( $numberOfEmployeesData['from'] ) &&
isset( $numberOfEmployeesData['to'] ) &&
0 < $numberOfEmployeesData['to']
) {
$data['numberOfEmployees'] = [
'@type' => 'QuantitativeValue',
'minValue' => $numberOfEmployeesData['from'],
'maxValue' => $numberOfEmployeesData['to']
];
}
if (
! $numberOfEmployeesData['isRange'] &&
! empty( $numberOfEmployeesData['number'] )
) {
$data['numberOfEmployees'] = [
'@type' => 'QuantitativeValue',
'value' => $numberOfEmployeesData['number']
];
}
$logo = $this->logo();
if ( ! empty( $logo ) ) {
$data['logo'] = $logo;
$data['image'] = [ '@id' => $data['logo']['@id'] ];
}
$socialUrls = array_values( $this->getOrganizationProfiles() );
if ( $socialUrls ) {
$data['sameAs'] = $socialUrls;
}
$data = $this->getAddonData( $data, 'kgOrganization' );
return $data;
}
/**
* Returns the logo data.
*
* @since 4.0.0
*
* @return array The logo data.
*/
public function logo() {
$logo = aioseo()->options->searchAppearance->global->schema->organizationLogo;
if ( $logo ) {
return $this->image( $logo, 'organizationLogo' );
}
$imageId = aioseo()->helpers->getSiteLogoId();
if ( $imageId ) {
return $this->image( $imageId, 'organizationLogo' );
}
return [];
}
} Schema/Graphs/Graph.php 0000666 00000004542 15113050716 0010752 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Traits as CommonTraits;
/**
* The base graph class.
*
* @since 4.0.0
*/
abstract class Graph {
use Traits\Image;
use CommonTraits\SocialProfiles;
/**
* The graph data to overwrite.
*
* @since 4.7.6
*
* @var array
*/
protected static $overwriteGraphData = [];
/**
* Returns the graph data.
*
* @since 4.0.0
*/
abstract public function get();
/**
* Iterates over a list of functions and sets the results as graph data.
*
* @since 4.0.13
*
* @param array $data The graph data to add to.
* @param array $dataFunctions List of functions to loop over, associated with a graph property.
* @return array $data The graph data with the results added.
*/
protected function getData( $data, $dataFunctions ) {
foreach ( $dataFunctions as $k => $f ) {
if ( ! method_exists( $this, $f ) ) {
continue;
}
$value = $this->$f();
if ( $value || in_array( $k, aioseo()->schema->nullableFields, true ) ) {
$data[ $k ] = $value;
}
}
return $data;
}
/**
* Decodes a multiselect field and returns the values.
*
* @since 4.6.4
*
* @param string $json The JSON encoded multiselect field.
* @return array The decoded values.
*/
protected function extractMultiselectTags( $json ) {
$tags = is_string( $json ) ? json_decode( $json ) : [];
if ( ! $tags ) {
return [];
}
return wp_list_pluck( $tags, 'value' );
}
/**
* Merges in data from our addon plugins.
*
* @since 4.5.6
* @version 4.6.4 Moved to main graph class.
*
* @param array $data The graph data.
* @return array The graph data.
*/
protected function getAddonData( $data, $className, $methodName = 'getAdditionalGraphData' ) {
$addonData = array_filter( aioseo()->addons->doAddonFunction( $className, $methodName, [
'postId' => get_the_ID(),
'data' => $data
] ) );
foreach ( $addonData as $addonGraphData ) {
$data = array_merge( $data, $addonGraphData );
}
return $data;
}
/**
* A way to overwrite the graph data.
*
* @since 4.7.6
*
* @param array $data The data to overwrite.
* @return void
*/
public static function setOverwriteGraphData( $data ) {
self::$overwriteGraphData[ static::class ] = $data;
}
} Schema/Graphs/Traits/Image.php 0000666 00000005462 15113050716 0012203 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\Traits;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Trait that handles images for the graphs.
*
* @since 4.2.5
*/
trait Image {
/**
* Builds the graph data for a given image with a given schema ID.
*
* @since 4.0.0
*
* @param int $imageId The image ID.
* @param string $graphId The graph ID (optional).
* @return array $data The image graph data.
*/
protected function image( $imageId, $graphId = '' ) {
$attachmentId = is_string( $imageId ) && ! is_numeric( $imageId ) ? aioseo()->helpers->attachmentUrlToPostId( $imageId ) : $imageId;
$imageUrl = wp_get_attachment_image_url( $attachmentId, 'full' );
$data = [
'@type' => 'ImageObject',
'url' => $imageUrl ? $imageUrl : $imageId,
];
if ( $graphId ) {
$baseUrl = aioseo()->schema->context['url'] ?? aioseo()->helpers->getUrl();
$data['@id'] = trailingslashit( $baseUrl ) . '#' . $graphId;
}
if ( ! $attachmentId ) {
return $data;
}
$metaData = wp_get_attachment_metadata( $attachmentId );
if ( $metaData && ! empty( $metaData['width'] ) && ! empty( $metaData['height'] ) ) {
$data['width'] = (int) $metaData['width'];
$data['height'] = (int) $metaData['height'];
}
$caption = $this->getImageCaption( $attachmentId );
if ( ! empty( $caption ) ) {
$data['caption'] = $caption;
}
return $data;
}
/**
* Get the image caption.
*
* @since 4.1.4
*
* @param int $attachmentId The attachment ID.
* @return string The caption.
*/
private function getImageCaption( $attachmentId ) {
$caption = wp_get_attachment_caption( $attachmentId );
if ( ! empty( $caption ) ) {
return $caption;
}
return get_post_meta( $attachmentId, '_wp_attachment_image_alt', true );
}
/**
* Returns the graph data for the avatar of a given user.
*
* @since 4.0.0
*
* @param int $userId The user ID.
* @param string $graphId The graph ID.
* @return array The graph data.
*/
protected function avatar( $userId, $graphId ) {
if ( ! get_option( 'show_avatars' ) ) {
return [];
}
$avatar = get_avatar_data( $userId );
if ( ! $avatar['found_avatar'] ) {
return [];
}
return array_filter( [
'@type' => 'ImageObject',
'@id' => aioseo()->schema->context['url'] . "#$graphId",
'url' => $avatar['url'],
'width' => $avatar['width'],
'height' => $avatar['height'],
'caption' => get_the_author_meta( 'display_name', $userId )
] );
}
/**
* Returns the graph data for the post's featured image.
*
* @since 4.2.5
*
* @return string The featured image URL.
*/
protected function getFeaturedImage() {
$post = aioseo()->helpers->getPost();
return has_post_thumbnail( $post ) ? $this->image( get_post_thumbnail_id() ) : '';
}
} Schema/Graphs/Article/Article.php 0000666 00000011770 15113050716 0012660 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\Article;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Schema\Graphs;
/**
* Article graph class.
*
* @since 4.0.0
*/
class Article extends Graphs\Graph {
/**
* Returns the graph data.
*
* @since 4.2.5
*
* @param Object $graphData The graph data.
* @return array The parsed graph data.
*/
public function get( $graphData = null ) {
$post = aioseo()->helpers->getPost();
if ( ! is_a( $post, 'WP_Post' ) ) {
return [];
}
$data = [
'@type' => 'Article',
'@id' => ! empty( $graphData->id ) ? aioseo()->schema->context['url'] . $graphData->id : aioseo()->schema->context['url'] . '#article',
'name' => ! empty( $graphData->properties->name ) ? $graphData->properties->name : aioseo()->schema->context['name'],
'headline' => ! empty( $graphData->properties->headline ) ? $graphData->properties->headline : get_the_title(),
'description' => ! empty( $graphData->properties->description ) ? $graphData->properties->description : '',
'author' => [
'@type' => 'Person',
'name' => ! empty( $graphData->properties->author->name ) ? $graphData->properties->author->name : get_the_author_meta( 'display_name' ),
'url' => ! empty( $graphData->properties->author->url ) ? $graphData->properties->author->url : '',
],
'publisher' => [ '@id' => trailingslashit( home_url() ) . '#' . aioseo()->options->searchAppearance->global->schema->siteRepresents ],
'image' => ! empty( $graphData->properties->image ) ? $this->image( $graphData->properties->image ) : $this->postImage( $post ),
'datePublished' => ! empty( $graphData->properties->dates->datePublished )
? mysql2date( DATE_W3C, $graphData->properties->dates->datePublished, false )
: mysql2date( DATE_W3C, $post->post_date, false ),
'dateModified' => ! empty( $graphData->properties->dates->dateModified )
? mysql2date( DATE_W3C, $graphData->properties->dates->dateModified, false )
: mysql2date( DATE_W3C, $post->post_modified, false ),
'inLanguage' => aioseo()->helpers->currentLanguageCodeBCP47(),
'commentCount' => get_comment_count( $post->ID )['approved'],
'mainEntityOfPage' => empty( $graphData ) ? [ '@id' => aioseo()->schema->context['url'] . '#webpage' ] : '',
'isPartOf' => empty( $graphData ) ? [ '@id' => aioseo()->schema->context['url'] . '#webpage' ] : ''
];
if ( empty( $graphData->properties->author->name ) ) {
if ( ! in_array( 'PersonAuthor', aioseo()->schema->graphs, true ) ) {
aioseo()->schema->graphs[] = 'PersonAuthor';
}
$data['author'] = [
'@id' => get_author_posts_url( $post->post_author ) . '#author'
];
}
if ( ! empty( $graphData->properties->keywords ) ) {
$keywords = json_decode( $graphData->properties->keywords, true );
$keywords = array_map( function ( $keywordObject ) {
return $keywordObject['value'];
}, $keywords );
$data['keywords'] = implode( ', ', $keywords );
}
if ( isset( $graphData->properties->dates->include ) && ! $graphData->properties->dates->include ) {
unset( $data['datePublished'] );
unset( $data['dateModified'] );
}
$postTaxonomies = get_post_taxonomies( $post );
$postTerms = [];
foreach ( $postTaxonomies as $taxonomy ) {
$terms = get_the_terms( $post, $taxonomy );
if ( $terms ) {
$postTerms = array_merge( $postTerms, wp_list_pluck( $terms, 'name' ) );
}
}
if ( ! empty( $postTerms ) ) {
$data['articleSection'] = implode( ', ', $postTerms );
}
$pageNumber = aioseo()->helpers->getPageNumber();
if ( 1 < $pageNumber ) {
$data['pagination'] = $pageNumber;
}
return $data;
}
/**
* Returns the graph data for the post image.
*
* @since 4.0.0
*
* @param \WP_Post $post The post object.
* @return array The image graph data.
*/
private function postImage( $post ) {
$featuredImage = $this->getFeaturedImage();
if ( $featuredImage ) {
return $featuredImage;
}
preg_match_all( '#<img[^>]+src="([^">]+)"#', (string) $post->post_content, $matches );
if ( isset( $matches[1] ) && isset( $matches[1][0] ) ) {
$url = aioseo()->helpers->removeImageDimensions( $matches[1][0] );
$imageId = aioseo()->helpers->attachmentUrlToPostId( $url );
if ( $imageId ) {
return $this->image( $imageId, 'articleImage' );
} else {
return $this->image( $url, 'articleImage' );
}
}
if ( 'organization' === aioseo()->options->searchAppearance->global->schema->siteRepresents ) {
$logo = ( new Graphs\KnowledgeGraph\KgOrganization() )->logo();
if ( ! empty( $logo ) ) {
$logo['@id'] = trailingslashit( home_url() ) . '#articleImage';
return $logo;
}
} else {
$avatar = $this->avatar( $post->post_author, 'articleImage' );
if ( $avatar ) {
return $avatar;
}
}
$imageId = aioseo()->helpers->getSiteLogoId();
if ( $imageId ) {
return $this->image( $imageId, 'articleImage' );
}
return [];
}
} Schema/Graphs/Article/NewsArticle.php 0000666 00000002315 15113050716 0013510 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\Article;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* News Article graph class.
*
* @since 4.0.0
*/
class NewsArticle extends Article {
/**
* Returns the graph data.
*
* @since 4.0.0
*
* @param object $graphData The graph data.
* @return array The parsed graph data.
*/
public function get( $graphData = null ) {
if ( ! empty( self::$overwriteGraphData[ __CLASS__ ] ) ) {
$graphData = json_decode( wp_json_encode( wp_parse_args( self::$overwriteGraphData[ __CLASS__ ], $graphData ) ) );
}
$data = parent::get( $graphData );
if ( ! $data ) {
return [];
}
$data['@type'] = 'NewsArticle';
$data['@id'] = ! empty( $graphData->id ) ? aioseo()->schema->context['url'] . $graphData->id : aioseo()->schema->context['url'] . '#newsarticle';
$date = ! empty( $graphData->properties->datePublished )
? mysql2date( 'F j, Y', $graphData->properties->datePublished, false )
: get_the_date( 'F j, Y' );
if ( $date ) {
// Translators: 1 - A date (e.g. September 2, 2022).
$data['dateline'] = sprintf( __( 'Published on %1$s.', 'all-in-one-seo-pack' ), $date );
}
return $data;
}
} Schema/Graphs/Article/BlogPosting.php 0000666 00000001307 15113050716 0013517 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Schema\Graphs\Article;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Blog Posting graph class.
*
* @since 4.0.0
*/
class BlogPosting extends Article {
/**
* Returns the graph data.
*
* @since 4.0.0
*
* @return object $graphData The graph data.
* @return array The parsed graph data.
*/
public function get( $graphData = null ) {
$data = parent::get( $graphData );
if ( ! $data ) {
return [];
}
$data['@type'] = 'BlogPosting';
$data['@id'] = ! empty( $graphData->id ) ? aioseo()->schema->context['url'] . $graphData->id : aioseo()->schema->context['url'] . '#blogposting';
return $data;
}
} Api/Integrations/Semrush.php 0000666 00000004331 15113050716 0012066 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api\Integrations;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\Semrush as SemrushIntegration;
/**
* Route class for the API.
*
* @since 4.0.16
*/
class Semrush {
/**
* Fetches the additional keyphrases.
*
* @since 4.0.16
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function semrushGetKeyphrases( $request ) {
$body = $request->get_json_params();
$keyphrases = SemrushIntegration::getKeyphrases( $body['keyphrase'], $body['database'] );
if ( false === $keyphrases ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'You may have sent too many requests to Semrush. Please wait a few minutes and try again.'
], 400 );
}
return new \WP_REST_Response( [
'success' => true,
'keyphrases' => $keyphrases
], 200 );
}
/**
* Authenticates with Semrush.
*
* @since 4.0.16
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function semrushAuthenticate( $request ) {
$body = $request->get_json_params();
if ( empty( $body['code'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Missing authorization code.'
], 400 );
}
$success = SemrushIntegration::authenticate( $body['code'] );
if ( ! $success ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Authentication failed.'
], 400 );
}
return new \WP_REST_Response( [
'success' => true,
'semrush' => aioseo()->internalOptions->integrations->semrush->all()
], 200 );
}
/**
* Refreshes the API tokens.
*
* @since 4.0.16
*
* @return \WP_REST_Response The response.
*/
public static function semrushRefresh() {
$success = SemrushIntegration::refreshTokens();
if ( ! $success ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'API tokens could not be refreshed.'
], 400 );
}
return new \WP_REST_Response( [
'success' => true,
'semrush' => aioseo()->internalOptions->integrations->semrush->all()
], 200 );
}
} Api/Integrations/WpCode.php 0000666 00000001656 15113050716 0011630 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api\Integrations;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\WpCode as WpCodeIntegration;
/**
* Route class for the API.
*
* @since 4.3.8
*/
class WpCode {
/**
* Load the WPCode Snippets from the library, if available.
*
* @since 4.3.8
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function getSnippets( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return new \WP_REST_Response( [
'success' => true,
'snippets' => WpCodeIntegration::loadWpCodeSnippets(),
'pluginInstalled' => WpCodeIntegration::isPluginInstalled(),
'pluginActive' => WpCodeIntegration::isPluginActive(),
'pluginNeedsUpdate' => WpCodeIntegration::pluginNeedsUpdate()
], 200 );
}
} Api/Network.php 0000666 00000002547 15113050716 0007432 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Route class for the API.
*
* @since 4.2.5
*/
class Network {
/**
* Save network robots rules.
*
* @since 4.2.5
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function saveNetworkRobots( $request ) {
$isNetwork = 'network' === $request->get_param( 'siteId' );
$siteId = $isNetwork ? aioseo()->helpers->getNetworkId() : (int) $request->get_param( 'siteId' );
$body = $request->get_json_params();
$rules = ! empty( $body['rules'] ) ? array_map( 'sanitize_text_field', $body['rules'] ) : [];
$enabled = isset( $body['enabled'] ) ? boolval( $body['enabled'] ) : null;
$searchAppearance = ! empty( $body['searchAppearance'] ) ? $body['searchAppearance'] : [];
aioseo()->helpers->switchToBlog( $siteId );
$options = $isNetwork ? aioseo()->networkOptions : aioseo()->options;
$enabled = null === $enabled ? $options->tools->robots->enable : $enabled;
$options->sanitizeAndSave( [
'tools' => [
'robots' => [
'enable' => $enabled,
'rules' => $rules
]
],
'searchAppearance' => $searchAppearance
] );
return new \WP_REST_Response( [
'success' => true
], 200 );
}
} Api/Plugins.php 0000666 00000010533 15113050716 0007414 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Route class for the API.
*
* @since 4.0.0
*/
class Plugins {
/**
* Installs plugins from vue.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function installPlugins( $request ) {
$error = esc_html__( 'Installation failed. Please check permissions and try again.', 'all-in-one-seo-pack' );
$body = $request->get_json_params();
$plugins = ! empty( $body['plugins'] ) ? $body['plugins'] : [];
$network = ! empty( $body['network'] ) ? $body['network'] : false;
if ( ! is_array( $plugins ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $error
], 400 );
}
if ( ! aioseo()->addons->canInstall() ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $error
], 400 );
}
$failed = [];
$completed = [];
foreach ( $plugins as $plugin ) {
if ( empty( $plugin['plugin'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $error
], 400 );
}
$result = aioseo()->addons->installAddon( $plugin['plugin'], $network );
if ( ! $result ) {
$failed[] = $plugin['plugin'];
} else {
$completed[ $plugin['plugin'] ] = $result;
}
}
return new \WP_REST_Response( [
'success' => true,
'completed' => $completed,
'failed' => $failed
], 200 );
}
/**
* Upgrade plugins from vue.
*
* @since 4.1.6
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function upgradePlugins( $request ) {
$error = esc_html__( 'Plugin update failed. Please check permissions and try again.', 'all-in-one-seo-pack' );
$body = $request->get_json_params();
$plugins = ! empty( $body['plugins'] ) ? $body['plugins'] : [];
$network = ! empty( $body['network'] ) ? $body['network'] : false;
if ( ! is_array( $plugins ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $error
], 400 );
}
if ( ! aioseo()->addons->canUpdate() ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $error
], 400 );
}
$failed = [];
$completed = [];
foreach ( $plugins as $plugin ) {
if ( empty( $plugin['plugin'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $error
], 400 );
}
$result = aioseo()->addons->upgradeAddon( $plugin['plugin'], $network );
if ( ! $result ) {
$failed[] = $plugin['plugin'];
} else {
$completed[ $plugin['plugin'] ] = aioseo()->addons->getAddon( $plugin['plugin'], true );
}
}
return new \WP_REST_Response( [
'success' => true,
'completed' => $completed,
'failed' => $failed
], 200 );
}
/**
* Deactivates plugins from vue.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function deactivatePlugins( $request ) {
$error = esc_html__( 'Deactivation failed. Please check permissions and try again.', 'all-in-one-seo-pack' );
$body = $request->get_json_params();
$plugins = ! empty( $body['plugins'] ) ? $body['plugins'] : [];
$network = ! empty( $body['network'] ) ? $body['network'] : false;
if ( ! is_array( $plugins ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $error
], 400 );
}
if ( ! current_user_can( 'install_plugins' ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $error
], 400 );
}
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$failed = [];
$completed = [];
foreach ( $plugins as $plugin ) {
if ( empty( $plugin['plugin'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $error
], 400 );
}
deactivate_plugins( $plugin['plugin'], false, $network );
$stillActive = $network ? is_plugin_active_for_network( $plugin['plugin'] ) : is_plugin_active( $plugin['plugin'] );
if ( $stillActive ) {
$failed[] = $plugin['plugin'];
}
$completed[] = $plugin['plugin'];
}
return new \WP_REST_Response( [
'success' => true,
'completed' => $completed,
'failed' => $failed
], 200 );
}
} Api/Tags.php 0000666 00000000604 15113050716 0006667 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Route class for the API.
*
* @since 4.0.0
*/
class Tags {
/**
* Get all Tags.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function getTags() {
return new \WP_REST_Response( aioseo()->tags->all( true ), 200 );
}
} Api/Migration.php 0000666 00000003575 15113050716 0007734 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Migration as CommonMigration;
use AIOSEO\Plugin\Common\Models;
/**
* Route class for the API.
*
* @since 4.0.6
*/
class Migration {
/**
* Resets blank title formats and retriggers the post/term meta migration.
*
* @since 4.0.6
*
* @return \WP_REST_Response The response.
*/
public static function fixBlankFormats() {
$oldOptions = ( new CommonMigration\OldOptions() )->oldOptions;
if ( ! $oldOptions ) {
return new \WP_REST_Response( [
'success' => true,
'message' => 'Could not load v3 options.'
], 400 );
}
$postTypes = aioseo()->helpers->getPublicPostTypes( true );
$taxonomies = aioseo()->helpers->getPublicTaxonomies( true );
foreach ( $oldOptions as $k => $v ) {
if ( ! preg_match( '/^aiosp_([a-zA-Z]*)_title_format$/', (string) $k, $match ) || ! empty( $v ) ) {
continue;
}
$objectName = $match[1];
if ( in_array( $objectName, $postTypes, true ) && aioseo()->dynamicOptions->searchAppearance->postTypes->has( $objectName ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$objectName->title = '#post_title #separator_sa #site_title';
continue;
}
if ( in_array( $objectName, $taxonomies, true ) && aioseo()->dynamicOptions->searchAppearance->taxonomies->has( $objectName ) ) {
aioseo()->dynamicOptions->searchAppearance->taxonomies->$objectName->title = '#taxonomy_title #separator_sa #site_title';
}
}
aioseo()->migration->redoMetaMigration();
Models\Notification::deleteNotificationByName( 'v3-migration-title-formats-blank' );
return new \WP_REST_Response( [
'success' => true,
'message' => 'Title formats have been reset; post/term migration has been scheduled.',
'notifications' => Models\Notification::getNotifications()
], 200 );
}
} Api/Settings.php 0000666 00000057350 15113050716 0007603 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Migration;
/**
* Route class for the API.
*
* @since 4.0.0
*/
class Settings {
/**
* Contents to import.
*
* @since 4.7.2
*
* @var array
*/
public static $importFile = [];
/**
* Update the settings.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function getOptions() {
return new \WP_REST_Response( [
'options' => aioseo()->options->all(),
'settings' => aioseo()->settings->all()
], 200 );
}
/**
* Toggles a card in the settings.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function toggleCard( $request ) {
$body = $request->get_json_params();
$card = ! empty( $body['card'] ) ? sanitize_text_field( $body['card'] ) : null;
$cards = aioseo()->settings->toggledCards;
if ( array_key_exists( $card, $cards ) ) {
$cards[ $card ] = ! $cards[ $card ];
aioseo()->settings->toggledCards = $cards;
}
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Toggles a radio in the settings.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function toggleRadio( $request ) {
$body = $request->get_json_params();
$radio = ! empty( $body['radio'] ) ? sanitize_text_field( $body['radio'] ) : null;
$value = ! empty( $body['value'] ) ? sanitize_text_field( $body['value'] ) : null;
$radios = aioseo()->settings->toggledRadio;
if ( array_key_exists( $radio, $radios ) ) {
$radios[ $radio ] = $value;
aioseo()->settings->toggledRadio = $radios;
}
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Dismisses an alert.
*
* @since 4.3.6
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function dismissAlert( $request ) {
$body = $request->get_json_params();
$alert = ! empty( $body['alert'] ) ? sanitize_text_field( $body['alert'] ) : null;
$alerts = aioseo()->settings->dismissedAlerts;
if ( array_key_exists( $alert, $alerts ) ) {
$alerts[ $alert ] = true;
aioseo()->settings->dismissedAlerts = $alerts;
}
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Toggles a table's items per page setting.
*
* @since 4.2.5
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function changeItemsPerPage( $request ) {
$body = $request->get_json_params();
$table = ! empty( $body['table'] ) ? sanitize_text_field( $body['table'] ) : null;
$value = ! empty( $body['value'] ) ? intval( $body['value'] ) : null;
$tables = aioseo()->settings->tablePagination;
if ( array_key_exists( $table, $tables ) ) {
$tables[ $table ] = $value;
aioseo()->settings->tablePagination = $tables;
}
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Dismisses the upgrade bar.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function hideUpgradeBar() {
aioseo()->settings->showUpgradeBar = false;
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Hides the Setup Wizard CTA.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function hideSetupWizard() {
aioseo()->settings->showSetupWizard = false;
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Save options from the front end.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function saveChanges( $request ) {
$body = $request->get_json_params();
$options = ! empty( $body['options'] ) ? $body['options'] : [];
$dynamicOptions = ! empty( $body['dynamicOptions'] ) ? $body['dynamicOptions'] : [];
$network = ! empty( $body['network'] ) ? (bool) $body['network'] : false;
$networkOptions = ! empty( $body['networkOptions'] ) ? $body['networkOptions'] : [];
// If this is the network admin, reset the options.
if ( $network ) {
aioseo()->networkOptions->sanitizeAndSave( $networkOptions );
} else {
aioseo()->options->sanitizeAndSave( $options );
aioseo()->dynamicOptions->sanitizeAndSave( $dynamicOptions );
}
// Re-initialize notices.
aioseo()->notices->init();
return new \WP_REST_Response( [
'success' => true,
'notifications' => Models\Notification::getNotifications()
], 200 );
}
/**
* Reset settings.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function resetSettings( $request ) {
$body = $request->get_json_params();
$settings = ! empty( $body['settings'] ) ? $body['settings'] : [];
$notAllowedOptions = aioseo()->access->getNotAllowedOptions();
foreach ( $settings as $setting ) {
$optionAccess = in_array( $setting, [ 'robots', 'blocker' ], true ) ? 'tools' : $setting;
if ( in_array( $optionAccess, $notAllowedOptions, true ) ) {
continue;
}
switch ( $setting ) {
case 'robots':
aioseo()->options->tools->robots->reset();
aioseo()->options->searchAppearance->advanced->unwantedBots->reset();
aioseo()->options->searchAppearance->advanced->searchCleanup->settings->preventCrawling = false;
break;
default:
if ( 'searchAppearance' === $setting ) {
aioseo()->robotsTxt->resetSearchAppearanceRules();
}
if ( aioseo()->options->has( $setting ) ) {
aioseo()->options->$setting->reset();
}
if ( aioseo()->dynamicOptions->has( $setting ) ) {
aioseo()->dynamicOptions->$setting->reset();
}
}
if ( 'access-control' === $setting ) {
aioseo()->access->addCapabilities();
}
}
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Import settings from external file.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function importSettings( $request ) {
$file = $request->get_file_params()['file'];
$isJSONFile = 'application/json' === $file['type'];
$isCSVFile = 'text/csv' === $file['type'];
$isOctetFile = 'application/octet-stream' === $file['type'];
if (
empty( $file['tmp_name'] ) ||
empty( $file['type'] ) ||
(
! $isJSONFile &&
! $isCSVFile &&
! $isOctetFile
)
) {
return new \WP_REST_Response( [
'success' => false
], 400 );
}
$contents = aioseo()->core->fs->getContents( $file['tmp_name'] );
if ( empty( $contents ) ) {
return new \WP_REST_Response( [
'success' => false
], 400 );
}
if ( $isJSONFile ) {
self::$importFile = json_decode( $contents, true );
}
if ( $isCSVFile ) {
// Transform the CSV content into the original JSON array.
self::$importFile = self::prepareCsvImport( $contents );
}
// If the file is invalid just return.
if ( empty( self::$importFile ) ) {
return new \WP_REST_Response( [
'success' => false
], 400 );
}
// Import settings.
if ( ! empty( self::$importFile['settings'] ) ) {
self::importSettingsFromFile( self::$importFile['settings'] );
}
// Import posts.
if ( ! empty( self::$importFile['postOptions'] ) ) {
self::importPostsFromFile( self::$importFile['postOptions'] );
}
// Import INI.
if ( $isOctetFile ) {
$response = aioseo()->importExport->importIniData( self::$importFile );
if ( ! $response ) {
return new \WP_REST_Response( [
'success' => false
], 400 );
}
}
return new \WP_REST_Response( [
'success' => true,
'options' => aioseo()->options->all()
], 200 );
}
/**
* Import settings from a file.
*
* @since 4.7.2
*
* @param array $settings The data to import.
*/
private static function importSettingsFromFile( $settings ) {
// Clean up the array removing options the user should not manage.
$notAllowedOptions = aioseo()->access->getNotAllowedOptions();
$settings = array_diff_key( $settings, $notAllowedOptions );
if ( ! empty( $settings['deprecated'] ) ) {
$settings['deprecated'] = array_diff_key( $settings['deprecated'], $notAllowedOptions );
}
// Remove any dynamic options and save them separately since this has been refactored.
$commonDynamic = [
'sitemap',
'searchAppearance',
'breadcrumbs',
'accessControl'
];
foreach ( $commonDynamic as $cd ) {
if ( ! empty( $settings[ $cd ]['dynamic'] ) ) {
$settings['dynamic'][ $cd ] = $settings[ $cd ]['dynamic'];
unset( $settings[ $cd ]['dynamic'] );
}
}
// These options have a very different structure so we'll do them separately.
if ( ! empty( $settings['social']['facebook']['general']['dynamic'] ) ) {
$settings['dynamic']['social']['facebook']['general'] = $settings['social']['facebook']['general']['dynamic'];
unset( $settings['social']['facebook']['general']['dynamic'] );
}
if ( ! empty( $settings['dynamic'] ) ) {
aioseo()->dynamicOptions->sanitizeAndSave( $settings['dynamic'] );
unset( $settings['dynamic'] );
}
if ( ! empty( $settings['tools']['robots']['rules'] ) ) {
$settings['tools']['robots']['rules'] = array_merge( aioseo()->robotsTxt->extractSearchAppearanceRules(), $settings['tools']['robots']['rules'] );
}
aioseo()->options->sanitizeAndSave( $settings );
}
/**
* Import posts from a file.
*
* @since 4.7.2
*
* @param array $postOptions The data to import.
*/
private static function importPostsFromFile( $postOptions ) {
$notAllowedFields = aioseo()->access->getNotAllowedPageFields();
foreach ( $postOptions as $postData ) {
if ( ! empty( $postData['posts'] ) ) {
foreach ( $postData['posts'] as $post ) {
unset( $post['id'] );
// Clean up the array removing fields the user should not manage.
$post = array_diff_key( $post, $notAllowedFields );
$thePost = Models\Post::getPost( $post['post_id'] );
// Remove primary term if the term is not attached to the post anymore.
if ( ! empty( $post['primary_term'] ) && aioseo()->helpers->isJsonString( $post['primary_term'] ) ) {
$primaryTerms = json_decode( $post['primary_term'], true );
foreach ( $primaryTerms as $tax => $termId ) {
$terms = wp_get_post_terms( $post['post_id'], $tax, [
'fields' => 'ids'
] );
if ( is_array( $terms ) && ! in_array( $termId, $terms, true ) ) {
unset( $primaryTerms[ $tax ] );
}
}
$post['primary_term'] = empty( $primaryTerms ) ? null : wp_json_encode( $primaryTerms );
}
// Remove FAQ Block schema if the block is not present in the post anymore.
if ( ! empty( $post['schema'] ) && aioseo()->helpers->isJsonString( $post['schema'] ) ) {
$schemas = json_decode( $post['schema'], true );
foreach ( $schemas['blockGraphs'] as $index => $block ) {
if ( 'aioseo/faq' !== $block['type'] ) {
continue;
}
$postBlocks = parse_blocks( get_the_content( null, false, $post['post_id'] ) );
$postFaqBlock = array_filter( $postBlocks, function( $block ) {
return 'aioseo/faq' === $block['blockName'];
} );
if ( empty( $postFaqBlock ) ) {
unset( $schemas['blockGraphs'][ $index ] );
}
}
$post['schema'] = wp_json_encode( $schemas );
}
$thePost->set( $post );
$thePost->save();
}
}
}
}
/**
* Prepare the content from CSV to the original JSON array to import.
*
* @since 4.7.2
*
* @param string $fileContent The Data to import.
* @return array The content.
*/
public static function prepareCSVImport( $fileContent ) {
$content = [];
$newContent = [
'postOptions' => null
];
$rows = str_getcsv( $fileContent, "\n" );
// Get the first row to check if the file has post_id or term_id.
$header = str_getcsv( $rows[0], ',' );
$header = aioseo()->helpers->sanitizeOption( $header );
// Check if the file has post_id or term_id.
$type = in_array( 'post_id', $header, true ) ? 'posts' : null;
$type = in_array( 'term_id', $header, true ) ? 'terms' : $type;
if ( ! $type ) {
return false;
}
// Remove header row.
unset( $rows[0] );
$jsonFields = [
'keywords',
'keyphrases',
'page_analysis',
'primary_term',
'og_article_tags',
'schema',
'options',
'open_ai',
'videos'
];
foreach ( $rows as $row ) {
$row = str_replace( '\\""', '\\"', $row );
$row = str_getcsv( $row, ',' );
foreach ( $row as $key => $value ) {
$key = aioseo()->helpers->sanitizeOption( $key );
if ( ! empty( $value ) && in_array( $header[ $key ], $jsonFields, true ) && ! aioseo()->helpers->isJsonString( $value ) ) {
continue;
} elseif ( '' === trim( $value ) ) {
$value = null;
}
$content[ $header [ $key ] ] = $value;
}
$newContent['postOptions']['content'][ $type ][] = $content;
}
return $newContent;
}
/**
* Export settings.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function exportSettings( $request ) {
$body = $request->get_json_params();
$settings = ! empty( $body['settings'] ) ? $body['settings'] : [];
$allSettings = [
'settings' => []
];
if ( empty( $settings ) ) {
return new \WP_REST_Response( [
'success' => false
], 400 );
}
$options = aioseo()->options->noConflict();
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
$notAllowedOptions = aioseo()->access->getNotAllowedOptions();
foreach ( $settings as $setting ) {
$optionAccess = in_array( $setting, [ 'robots', 'blocker' ], true ) ? 'tools' : $setting;
if ( in_array( $optionAccess, $notAllowedOptions, true ) ) {
continue;
}
switch ( $setting ) {
case 'robots':
$allSettings['settings']['tools']['robots'] = $options->tools->robots->all();
// Search Appearance settings that are also found in the robots settings.
if ( empty( $allSettings['settings']['searchAppearance']['advanced'] ) ) {
$allSettings['settings']['searchAppearance']['advanced'] = [
'unwantedBots' => $options->searchAppearance->advanced->unwantedBots->all(),
'searchCleanup' => [
'settings' => [
'preventCrawling' => $options->searchAppearance->advanced->searchCleanup->settings->preventCrawling
]
]
];
}
break;
default:
if ( $options->has( $setting ) ) {
$allSettings['settings'][ $setting ] = $options->$setting->all();
}
// If there are related dynamic settings, let's include them.
if ( $dynamicOptions->has( $setting ) ) {
$allSettings['settings']['dynamic'][ $setting ] = $dynamicOptions->$setting->all();
}
// It there is a related deprecated $setting, include it.
if ( $options->deprecated->has( $setting ) ) {
$allSettings['settings']['deprecated'][ $setting ] = $options->deprecated->$setting->all();
}
break;
}
}
return new \WP_REST_Response( [
'success' => true,
'settings' => $allSettings
], 200 );
}
/**
* Export post data.
*
* @since 4.7.2
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function exportContent( $request ) {
$body = $request->get_json_params();
$postOptions = $body['postOptions'] ?? [];
$typeFile = $body['typeFile'] ?? false;
$siteId = (int) ( $body['siteId'] ?? get_current_blog_id() );
$contentPostType = null;
$return = true;
try {
aioseo()->helpers->switchToBlog( $siteId );
// Get settings from post types selected.
if ( ! empty( $postOptions ) ) {
$fieldsToExclude = [
'seo_score' => '',
'schema_type' => '',
'schema_type_options' => '',
'images' => '',
'image_scan_date' => '',
'videos' => '',
'video_thumbnail' => '',
'video_scan_date' => '',
'link_scan_date' => '',
'link_suggestions_scan_date' => '',
'local_seo' => '',
'options' => '',
'open_ai' => ''
];
$notAllowed = array_merge( aioseo()->access->getNotAllowedPageFields(), $fieldsToExclude );
$posts = self::getPostTypesData( $postOptions, $notAllowed );
// Generate content to CSV or JSON.
if ( ! empty( $posts ) ) {
// Change the order of keys so the post_title shows up at the beginning.
$data = [];
foreach ( $posts as $p ) {
$item = [
'id' => '',
'post_id' => '',
'post_title' => '',
'title' => ''
];
$p['title'] = aioseo()->helpers->decodeHtmlEntities( $p['title'] );
$p['post_title'] = aioseo()->helpers->decodeHtmlEntities( $p['post_title'] );
$data[] = array_merge( $item, $p );
}
if ( 'csv' === $typeFile ) {
$contentPostType = self::dataToCsv( $data );
}
if ( 'json' === $typeFile ) {
$contentPostType['postOptions']['content']['posts'] = $data;
}
}
}
} catch ( \Throwable $th ) {
$return = false;
}
return new \WP_REST_Response( [
'success' => $return,
'postTypeData' => $contentPostType
], 200 );
}
/**
* Returns the posts of specific post types.
*
* @since 4.7.2
*
* @param array $postOptions The post types to get data from.
* @param array $notAllowedFields An array of fields not allowed to be returned.
* @return array The posts.
*/
private static function getPostTypesData( $postOptions, $notAllowedFields = [] ) {
$posts = aioseo()->core->db->start( 'aioseo_posts as ap' )
->select( 'ap.*, p.post_title' )
->join( 'posts as p', 'ap.post_id = p.ID' )
->whereIn( 'p.post_type', $postOptions )
->orderBy( 'ap.id' )
->run()
->result();
if ( ! empty( $notAllowedFields ) ) {
foreach ( $posts as $key => &$p ) {
$p = array_diff_key( (array) $p, $notAllowedFields );
if ( count( $p ) <= 2 ) {
unset( $posts[ $key ] );
}
}
}
return $posts;
}
/**
* Returns a CSV string.
*
* @since 4.7.2
*
* @param array $data An array of data to transform into a CSV.
* @return string The CSV string.
*/
public static function dataToCsv( $data ) {
// Get the header row.
$csvString = implode( ',', array_keys( (array) $data[0] ) ) . "\r\n";
// Get the content rows.
foreach ( $data as $row ) {
$row = (array) $row;
foreach ( $row as &$value ) {
if ( aioseo()->helpers->isJsonString( $value ) ) {
$value = '"' . str_replace( '"', '""', $value ) . '"';
} elseif ( false !== strpos( (string) $value, ',' ) ) {
$value = '"' . $value . '"';
}
}
$csvString .= implode( ',', $row ) . "\r\n";
}
return $csvString;
}
/**
* Import other plugin settings.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function importPlugins( $request ) {
$body = $request->get_json_params();
$plugins = ! empty( $body['plugins'] ) ? $body['plugins'] : [];
foreach ( $plugins as $plugin ) {
aioseo()->importExport->startImport( $plugin['plugin'], $plugin['settings'] );
}
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Executes a given administrative task.
*
* @since 4.1.2
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function doTask( $request ) {
$body = $request->get_json_params();
$action = ! empty( $body['action'] ) ? $body['action'] : '';
$data = ! empty( $body['data'] ) ? $body['data'] : [];
$network = ! empty( $body['network'] ) ? boolval( $body['network'] ) : false;
$siteId = ! empty( $body['siteId'] ) ? intval( $body['siteId'] ) : false;
$siteOrNetwork = empty( $siteId ) ? aioseo()->helpers->getNetworkId() : $siteId; // If we don't have a siteId, we will use the networkId.
// When on network admin page and no siteId, it is supposed to perform on network level.
if ( $network && 'clear-cache' === $action && empty( $siteId ) ) {
aioseo()->core->networkCache->clear();
return new \WP_REST_Response( [
'success' => true
], 200 );
}
// Switch to the right blog before processing any task.
aioseo()->helpers->switchToBlog( $siteOrNetwork );
switch ( $action ) {
// General
case 'clear-cache':
aioseo()->core->cache->clear();
break;
case 'clear-plugin-updates-transient':
delete_site_transient( 'update_plugins' );
break;
case 'readd-capabilities':
aioseo()->access->addCapabilities();
break;
case 'reset-data':
aioseo()->uninstall->dropData( true );
aioseo()->internalOptions->database->installedTables = '';
aioseo()->internalOptions->internal->lastActiveVersion = '4.0.0';
aioseo()->internalOptions->save( true );
aioseo()->updates->addInitialCustomTablesForV4();
break;
// Sitemap
case 'clear-image-data':
aioseo()->sitemap->query->resetImages();
break;
// Migrations
case 'rerun-migrations':
aioseo()->internalOptions->database->installedTables = '';
aioseo()->internalOptions->internal->lastActiveVersion = '4.0.0';
aioseo()->internalOptions->save( true );
break;
case 'rerun-addon-migrations':
aioseo()->internalOptions->database->installedTables = '';
foreach ( $data as $sku ) {
$convertedSku = aioseo()->helpers->dashesToCamelCase( $sku );
if (
function_exists( $convertedSku ) &&
isset( $convertedSku()->internalOptions )
) {
$convertedSku()->internalOptions->internal->lastActiveVersion = '0.0';
}
}
break;
case 'restart-v3-migration':
Migration\Helpers::redoMigration();
break;
// Old Issues
case 'remove-duplicates':
aioseo()->updates->removeDuplicateRecords();
break;
case 'unescape-data':
aioseo()->admin->scheduleUnescapeData();
break;
// Deprecated Options
case 'deprecated-options':
// Check if the user is forcefully wanting to add a deprecated option.
$allDeprecatedOptions = aioseo()->internalOptions->getAllDeprecatedOptions() ?: [];
$enableOptions = array_keys( array_filter( $data ) );
$enabledDeprecated = array_intersect( $allDeprecatedOptions, $enableOptions );
aioseo()->internalOptions->internal->deprecatedOptions = array_values( $enabledDeprecated );
aioseo()->internalOptions->save( true );
break;
case 'aioseo-reset-seoboost-logins':
aioseo()->writingAssistant->seoBoost->resetLogins();
break;
default:
aioseo()->helpers->restoreCurrentBlog();
return new \WP_REST_Response( [
'success' => true,
'error' => 'The given action isn\'t defined.'
], 400 );
}
// Revert back to the current blog after processing to avoid conflict with other actions.
aioseo()->helpers->restoreCurrentBlog();
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Change Sem Rush Focus Keyphrase default country.
*
* @since 4.7.5
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function changeSemrushCountry( $request ) {
$body = $request->get_json_params();
$country = ! empty( $body['value'] ) ? sanitize_text_field( $body['value'] ) : 'US';
aioseo()->settings->semrushCountry = $country;
return new \WP_REST_Response( [
'success' => true
], 200 );
}
} Api/Wizard.php 0000666 00000043654 15113050716 0007245 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Route class for the API.
*
* @since 4.0.0
*/
class Wizard {
/**
* Save the wizard information.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function saveWizard( $request ) {
$body = $request->get_json_params();
$section = ! empty( $body['section'] ) ? sanitize_text_field( $body['section'] ) : null;
$wizard = ! empty( $body['wizard'] ) ? $body['wizard'] : null;
$network = ! empty( $body['network'] ) ? $body['network'] : false;
$options = aioseo()->options->noConflict();
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
aioseo()->internalOptions->internal->wizard = wp_json_encode( $wizard );
// Process the importers.
if ( 'importers' === $section && ! empty( $wizard['importers'] ) ) {
$importers = $wizard['importers'];
try {
foreach ( $importers as $plugin ) {
aioseo()->importExport->startImport( $plugin, [
'settings',
'postMeta',
'termMeta'
] );
}
} catch ( \Exception $e ) {
// Import failed. Let's create a notification but move on.
$notification = Models\Notification::getNotificationByName( 'import-failed' );
if ( ! $notification->exists() ) {
Models\Notification::addNotification( [
'slug' => uniqid(),
'notification_name' => 'import-failed',
'title' => __( 'SEO Plugin Import Failed', 'all-in-one-seo-pack' ),
'content' => __( 'Unfortunately, there was an error importing your SEO plugin settings. This could be due to an incompatibility in the version installed. Make sure you are on the latest version of the plugin and try again.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
'type' => 'error',
'level' => [ 'all' ],
'button1_label' => __( 'Try Again', 'all-in-one-seo-pack' ),
'button1_action' => 'http://route#aioseo-tools&aioseo-scroll=aioseo-import-others&aioseo-highlight=aioseo-import-others:import-export',
'start' => gmdate( 'Y-m-d H:i:s' )
] );
}
}
}
// Save the category section.
if (
( 'category' === $section || 'searchAppearance' === $section ) && // We allow the user to update the site title/description in search appearance.
! empty( $wizard['category'] )
) {
$category = $wizard['category'];
if ( ! empty( $category['category'] ) ) {
aioseo()->internalOptions->internal->category = $category['category'];
}
if ( ! empty( $category['categoryOther'] ) ) {
aioseo()->internalOptions->internal->categoryOther = $category['categoryOther'];
}
// If the home page is a static page, let's find and set that,
// otherwise set our home page settings.
$staticHomePage = 'page' === get_option( 'show_on_front' ) ? get_post( get_option( 'page_on_front' ) ) : null;
if ( ! empty( $staticHomePage ) ) {
$update = false;
$page = Models\Post::getPost( $staticHomePage->ID );
if ( ! empty( $category['siteTitle'] ) ) {
$update = true;
$page->title = $category['siteTitle'];
}
if ( ! empty( $category['metaDescription'] ) ) {
$update = true;
$page->description = $category['metaDescription'];
}
if ( $update ) {
$page->save();
}
}
if ( empty( $staticHomePage ) ) {
if ( ! empty( $category['siteTitle'] ) ) {
$options->searchAppearance->global->siteTitle = $category['siteTitle'];
}
if ( ! empty( $category['metaDescription'] ) ) {
$options->searchAppearance->global->metaDescription = $category['metaDescription'];
}
}
}
// Save the additional information section.
if ( 'additionalInformation' === $section && ! empty( $wizard['additionalInformation'] ) ) {
$additionalInformation = $wizard['additionalInformation'];
if ( ! empty( $additionalInformation['siteRepresents'] ) ) {
$options->searchAppearance->global->schema->siteRepresents = $additionalInformation['siteRepresents'];
}
if ( ! empty( $additionalInformation['person'] ) ) {
$options->searchAppearance->global->schema->person = $additionalInformation['person'];
}
if ( ! empty( $additionalInformation['organizationName'] ) ) {
$options->searchAppearance->global->schema->organizationName = $additionalInformation['organizationName'];
}
if ( ! empty( $additionalInformation['organizationDescription'] ) ) {
$options->searchAppearance->global->schema->organizationDescription = $additionalInformation['organizationDescription'];
}
if ( ! empty( $additionalInformation['phone'] ) ) {
$options->searchAppearance->global->schema->phone = $additionalInformation['phone'];
}
if ( ! empty( $additionalInformation['organizationLogo'] ) ) {
$options->searchAppearance->global->schema->organizationLogo = $additionalInformation['organizationLogo'];
}
if ( ! empty( $additionalInformation['personName'] ) ) {
$options->searchAppearance->global->schema->personName = $additionalInformation['personName'];
}
if ( ! empty( $additionalInformation['personLogo'] ) ) {
$options->searchAppearance->global->schema->personLogo = $additionalInformation['personLogo'];
}
if ( ! empty( $additionalInformation['socialShareImage'] ) ) {
$options->social->facebook->general->defaultImagePosts = $additionalInformation['socialShareImage'];
$options->social->twitter->general->defaultImagePosts = $additionalInformation['socialShareImage'];
}
if ( ! empty( $additionalInformation['social'] ) && ! empty( $additionalInformation['social']['profiles'] ) ) {
$profiles = $additionalInformation['social']['profiles'];
if ( ! empty( $profiles['sameUsername'] ) ) {
$sameUsername = $profiles['sameUsername'];
if ( isset( $sameUsername['enable'] ) ) {
$options->social->profiles->sameUsername->enable = $sameUsername['enable'];
}
if ( ! empty( $sameUsername['username'] ) ) {
$options->social->profiles->sameUsername->username = $sameUsername['username'];
}
if ( ! empty( $sameUsername['included'] ) ) {
$options->social->profiles->sameUsername->included = $sameUsername['included'];
}
}
if ( ! empty( $profiles['urls'] ) ) {
$urls = $profiles['urls'];
if ( ! empty( $urls['facebookPageUrl'] ) ) {
$options->social->profiles->urls->facebookPageUrl = $urls['facebookPageUrl'];
}
if ( ! empty( $urls['twitterUrl'] ) ) {
$options->social->profiles->urls->twitterUrl = $urls['twitterUrl'];
}
if ( ! empty( $urls['instagramUrl'] ) ) {
$options->social->profiles->urls->instagramUrl = $urls['instagramUrl'];
}
if ( ! empty( $urls['tiktokUrl'] ) ) {
$options->social->profiles->urls->tiktokUrl = $urls['tiktokUrl'];
}
if ( ! empty( $urls['pinterestUrl'] ) ) {
$options->social->profiles->urls->pinterestUrl = $urls['pinterestUrl'];
}
if ( ! empty( $urls['youtubeUrl'] ) ) {
$options->social->profiles->urls->youtubeUrl = $urls['youtubeUrl'];
}
if ( ! empty( $urls['linkedinUrl'] ) ) {
$options->social->profiles->urls->linkedinUrl = $urls['linkedinUrl'];
}
if ( ! empty( $urls['tumblrUrl'] ) ) {
$options->social->profiles->urls->tumblrUrl = $urls['tumblrUrl'];
}
if ( ! empty( $urls['yelpPageUrl'] ) ) {
$options->social->profiles->urls->yelpPageUrl = $urls['yelpPageUrl'];
}
if ( ! empty( $urls['soundCloudUrl'] ) ) {
$options->social->profiles->urls->soundCloudUrl = $urls['soundCloudUrl'];
}
if ( ! empty( $urls['wikipediaUrl'] ) ) {
$options->social->profiles->urls->wikipediaUrl = $urls['wikipediaUrl'];
}
if ( ! empty( $urls['myspaceUrl'] ) ) {
$options->social->profiles->urls->myspaceUrl = $urls['myspaceUrl'];
}
if ( ! empty( $urls['googlePlacesUrl'] ) ) {
$options->social->profiles->urls->googlePlacesUrl = $urls['googlePlacesUrl'];
}
if ( ! empty( $urls['wordPressUrl'] ) ) {
$options->social->profiles->urls->wordPressUrl = $urls['wordPressUrl'];
}
if ( ! empty( $urls['blueskyUrl'] ) ) {
$options->social->profiles->urls->blueskyUrl = $urls['blueskyUrl'];
}
if ( ! empty( $urls['threadsUrl'] ) ) {
$options->social->profiles->urls->threadsUrl = $urls['threadsUrl'];
}
}
}
return new \WP_REST_Response( [
'success' => true
], 200 );
}
// Save the features section.
if ( 'features' === $section && ! empty( $wizard['features'] ) ) {
self::installPlugins( $wizard['features'], $network );
if ( in_array( 'email-reports', $wizard['features'], true ) ) {
$options->advanced->emailSummary->enable = true;
}
}
// Save the search appearance section.
if ( 'searchAppearance' === $section && ! empty( $wizard['searchAppearance'] ) ) {
$searchAppearance = $wizard['searchAppearance'];
if ( isset( $searchAppearance['underConstruction'] ) ) {
update_option( 'blog_public', ! $searchAppearance['underConstruction'] );
}
if (
! empty( $searchAppearance['postTypes'] ) &&
! empty( $searchAppearance['postTypes']['postTypes'] )
) {
// Robots.
if ( ! empty( $searchAppearance['postTypes']['postTypes']['all'] ) ) {
foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
if ( $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
$dynamicOptions->searchAppearance->postTypes->$postType->show = true;
$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = true;
$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = false;
}
}
} else {
foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
if ( $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
if ( in_array( $postType, (array) $searchAppearance['postTypes']['postTypes']['included'], true ) ) {
$dynamicOptions->searchAppearance->postTypes->$postType->show = true;
$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = true;
$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = false;
} else {
$dynamicOptions->searchAppearance->postTypes->$postType->show = false;
$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = false;
$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = true;
}
}
}
}
// Sitemaps.
if ( isset( $searchAppearance['postTypes']['postTypes']['all'] ) ) {
$options->sitemap->general->postTypes->all = $searchAppearance['postTypes']['postTypes']['all'];
}
if ( isset( $searchAppearance['postTypes']['postTypes']['included'] ) ) {
$options->sitemap->general->postTypes->included = $searchAppearance['postTypes']['postTypes']['included'];
}
}
if ( isset( $searchAppearance['multipleAuthors'] ) ) {
$options->searchAppearance->archives->author->show = $searchAppearance['multipleAuthors'];
$options->searchAppearance->archives->author->advanced->robotsMeta->default = $searchAppearance['multipleAuthors'];
$options->searchAppearance->archives->author->advanced->robotsMeta->noindex = ! $searchAppearance['multipleAuthors'];
}
if ( isset( $searchAppearance['redirectAttachmentPages'] ) && $dynamicOptions->searchAppearance->postTypes->has( 'attachment' ) ) {
$dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = $searchAppearance['redirectAttachmentPages'] ? 'attachment' : 'disabled';
}
if ( isset( $searchAppearance['emailReports'] ) ) {
$options->advanced->emailSummary->enable = $searchAppearance['emailReports'];
}
}
// Save the smart recommendations section.
if ( 'smartRecommendations' === $section && ! empty( $wizard['smartRecommendations'] ) ) {
$smartRecommendations = $wizard['smartRecommendations'];
if ( ! empty( $smartRecommendations['accountInfo'] ) && ! aioseo()->internalOptions->internal->siteAnalysis->connectToken ) {
$url = defined( 'AIOSEO_CONNECT_DIRECT_URL' ) ? AIOSEO_CONNECT_DIRECT_URL : 'https://aioseo.com/wp-json/aioseo-lite-connect/v1/connect/';
$response = wp_remote_post( $url, [
'timeout' => 10,
'headers' => array_merge( [
'Content-Type' => 'application/json'
], aioseo()->helpers->getApiHeaders() ),
'user-agent' => aioseo()->helpers->getApiUserAgent(),
'body' => wp_json_encode( [
'accountInfo' => $smartRecommendations['accountInfo'],
'homeurl' => home_url()
] )
] );
$token = json_decode( wp_remote_retrieve_body( $response ) );
if ( ! empty( $token->token ) ) {
aioseo()->internalOptions->internal->siteAnalysis->connectToken = $token->token;
}
}
}
return new \WP_REST_Response( [
'success' => true,
'options' => aioseo()->options->all()
], 200 );
}
/**
* Install all plugins that were selected in the features page of the Setup Wizard.
*
* @since 4.5.5
*
* @param array $features The features that were selected.
* @param bool $network Whether to install the plugins on the network.
* @return void
*/
private static function installPlugins( $features, $network ) {
$pluginData = aioseo()->helpers->getPluginData();
if ( in_array( 'analytics', $features, true ) ) {
self::installMonsterInsights( $network );
}
if ( in_array( 'conversion-tools', $features, true ) && ! $pluginData['optinMonster']['activated'] ) {
self::installOptinMonster( $network );
}
if ( in_array( 'broken-link-checker', $features, true ) && ! $pluginData['brokenLinkChecker']['activated'] ) {
self::installBlc( $network );
}
}
/**
* Installs the MonsterInsights plugin.
*
* @since 4.5.5
*
* @param bool $network Whether to install the plugin on the network.
* @return void
*/
private static function installMonsterInsights( $network ) {
$pluginData = aioseo()->helpers->getPluginData();
$args = [
'id' => 'miLite',
'pluginName' => 'MonsterInsights',
'pluginLongName' => 'MonsterInsights Analytics',
'notification-name' => 'install-mi'
];
// If MI Pro is active, bail.
if ( $pluginData['miPro']['activated'] ) {
return;
}
// If MI Pro is installed but not active, activate MI Pro.
if ( $pluginData['miPro']['installed'] ) {
$args['id'] = 'miPro';
}
if ( self::installPlugin( $args, $network ) ) {
delete_transient( '_monsterinsights_activation_redirect' );
}
}
/**
* Installs the OptinMonster plugin.
*
* @since 4.5.5
*
* @param bool $network Whether to install the plugin on the network.
* @return void
*/
private static function installOptinMonster( $network ) {
$args = [
'id' => 'optinMonster',
'pluginName' => 'OptinMonster',
'pluginLongName' => 'OptinMonster Conversion Tools',
'notification-name' => 'install-om'
];
if ( self::installPlugin( $args, $network ) ) {
delete_transient( 'optin_monster_api_activation_redirect' );
}
}
/**
* Installs the Broken Link Checker plugin.
*
* @since 4.5.5
*
* @param bool $network Whether to install the plugin on the network.
* @return void
*/
private static function installBlc( $network ) {
$args = [
'id' => 'brokenLinkChecker',
'pluginName' => 'Broken Link Checker',
'notification-name' => 'install-blc'
];
if ( self::installPlugin( $args, $network ) && function_exists( 'aioseoBrokenLinkChecker' ) ) {
aioseoBrokenLinkChecker()->core->cache->delete( 'activation_redirect' );
}
}
/**
* Helper method to install plugins through the Setup Wizard.
* Creates a notification if the plugin can't be installed.
*
* @since 4.5.5
*
* @param array $args The plugin arguments.
* @param bool $network Whether to install the plugin on the network.
* @return bool Whether the plugin was installed.
*/
private static function installPlugin( $args, $network = false ) {
if ( aioseo()->addons->canInstall() ) {
return aioseo()->addons->installAddon( $args['id'], $network );
}
$pluginData = aioseo()->helpers->getPluginData();
$notification = Models\Notification::getNotificationByName( $args['notification-name'] );
if ( ! $notification->exists() ) {
Models\Notification::addNotification( [
'slug' => uniqid(),
'notification_name' => $args['notification-name'],
'title' => sprintf(
// Translators: 1 - A plugin name (e.g. "MonsterInsights", "Broken Link Checker", etc.).
__( 'Install %1$s', 'all-in-one-seo-pack' ),
$args['pluginName']
),
'content' => sprintf(
// Translators: 1 - A plugin name (e.g. "MonsterInsights", "Broken Link Checker", etc.), 2 - The plugin short name ("AIOSEO").
__( 'You selected to install the free %1$s plugin during the setup of %2$s, but there was an issue during installation. Click below to manually install.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
AIOSEO_PLUGIN_SHORT_NAME,
! empty( $args['pluginLongName'] ) ? $args['pluginLongName'] : $args['pluginName']
),
'type' => 'info',
'level' => [ 'all' ],
'button1_label' => sprintf(
// Translators: 1 - A plugin name (e.g. "MonsterInsights", "Broken Link Checker", etc.).
__( 'Install %1$s', 'all-in-one-seo-pack' ),
$args['pluginName']
),
'button1_action' => $pluginData[ $args['id'] ]['wpLink'],
'button2_label' => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
'button2_action' => "http://action#notification/{$args['notification-name']}-reminder",
'start' => gmdate( 'Y-m-d H:i:s' )
] );
}
return false;
}
} Api/Notifications.php 0000666 00000010563 15113050716 0010607 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Route class for the API.
*
* @since 4.0.0
*/
class Notifications {
/**
* Extend the start date of a notice.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function blogVisibilityReminder() {
return self::reminder( 'blog-visibility' );
}
/**
* Extend the start date of a notice.
*
* @since 4.0.5
*
* @return \WP_REST_Response The response.
*/
public static function descriptionFormatReminder() {
return self::reminder( 'description-format' );
}
/**
* Extend the start date of a notice.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function installMiReminder() {
return self::reminder( 'install-mi' );
}
/**
* Extend the start date of a notice.
*
* @since 4.2.1
*
* @return \WP_REST_Response The response.
*/
public static function installOmReminder() {
return self::reminder( 'install-om' );
}
/**
* Extend the start date of a notice.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function installAddonsReminder() {
return self::reminder( 'install-addons' );
}
/**
* Extend the start date of a notice.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function installImageSeoReminder() {
return self::reminder( 'install-aioseo-image-seo' );
}
/**
* Extend the start date of a notice.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function installLocalBusinessReminder() {
return self::reminder( 'install-aioseo-local-business' );
}
/**
* Extend the start date of a notice.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function installNewsSitemapReminder() {
return self::reminder( 'install-aioseo-news-sitemap' );
}
/**
* Extend the start date of a notice.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function installVideoSitemapReminder() {
return self::reminder( 'install-aioseo-video-sitemap' );
}
/**
* Extend the start date of a notice.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function conflictingPluginsReminder() {
return self::reminder( 'conflicting-plugins' );
}
/**
* Extend the start date of a notice.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function migrationCustomFieldReminder() {
return self::reminder( 'v3-migration-custom-field' );
}
/**
* Extend the start date of a notice.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function migrationSchemaNumberReminder() {
return self::reminder( 'v3-migration-schema-number' );
}
/**
* This allows us to not repeat code over and over.
*
* @since 4.0.0
*
* @param string $slug The slug of the reminder.
* @return \WP_REST_Response The response.
*/
protected static function reminder( $slug ) {
aioseo()->notices->remindMeLater( $slug );
return new \WP_REST_Response( [
'success' => true,
'notifications' => Models\Notification::getNotifications()
], 200 );
}
/**
* Dismiss notifications.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function dismissNotifications( $request ) {
$slugs = $request->get_json_params();
$notifications = aioseo()->core->db
->start( 'aioseo_notifications' )
->whereIn( 'slug', $slugs )
->run()
->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' );
foreach ( $notifications as $notification ) {
$notification->dismissed = 1;
$notification->save();
}
// Dismiss static notifications.
if ( in_array( 'notification-review', $slugs, true ) ) {
update_user_meta( get_current_user_id(), '_aioseo_notification_plugin_review_dismissed', '3' );
}
if ( in_array( 'notification-review-delay', $slugs, true ) ) {
update_user_meta( get_current_user_id(), '_aioseo_notification_plugin_review_dismissed', strtotime( '+1 week' ) );
}
return new \WP_REST_Response( [
'success' => true,
'notifications' => Models\Notification::getNotifications()
], 200 );
}
} Api/Analyze.php 0000666 00000015146 15113050716 0007403 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models\SeoAnalyzerResult;
/**
* Route class for the API.
*
* @since 4.0.0
*/
class Analyze {
/**
* Analyzes the site for SEO.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function analyzeSite( $request ) {
$body = $request->get_json_params();
$analyzeUrl = ! empty( $body['url'] ) ? esc_url_raw( urldecode( $body['url'] ) ) : null;
$refreshResults = ! empty( $body['refresh'] ) ? (bool) $body['refresh'] : false;
$analyzeOrHomeUrl = ! empty( $analyzeUrl ) ? $analyzeUrl : home_url();
$responseCode = null === aioseo()->core->cache->get( 'analyze_site_code' ) ? [] : aioseo()->core->cache->get( 'analyze_site_code' );
$responseBody = null === aioseo()->core->cache->get( 'analyze_site_body' ) ? [] : aioseo()->core->cache->get( 'analyze_site_body' );
if (
empty( $responseCode ) ||
empty( $responseCode[ $analyzeOrHomeUrl ] ) ||
empty( $responseBody ) ||
empty( $responseBody[ $analyzeOrHomeUrl ] ) ||
$refreshResults
) {
$token = aioseo()->internalOptions->internal->siteAnalysis->connectToken;
$url = defined( 'AIOSEO_ANALYZE_URL' ) ? AIOSEO_ANALYZE_URL : 'https://analyze.aioseo.com';
$response = aioseo()->helpers->wpRemotePost( $url . '/v3/analyze/', [
'timeout' => 60,
'headers' => [
'X-AIOSEO-Key' => $token,
'Content-Type' => 'application/json'
],
'body' => wp_json_encode( [
'url' => $analyzeOrHomeUrl
] ),
] );
$responseCode[ $analyzeOrHomeUrl ] = wp_remote_retrieve_response_code( $response );
$responseBody[ $analyzeOrHomeUrl ] = json_decode( wp_remote_retrieve_body( $response ), true );
aioseo()->core->cache->update( 'analyze_site_code', $responseCode, 10 * MINUTE_IN_SECONDS );
aioseo()->core->cache->update( 'analyze_site_body', $responseBody, 10 * MINUTE_IN_SECONDS );
}
if ( 200 !== $responseCode[ $analyzeOrHomeUrl ] || empty( $responseBody[ $analyzeOrHomeUrl ]['success'] ) || ! empty( $responseBody[ $analyzeOrHomeUrl ]['error'] ) ) {
if ( ! empty( $responseBody[ $analyzeOrHomeUrl ]['error'] ) && 'invalid-token' === $responseBody[ $analyzeOrHomeUrl ]['error'] ) {
aioseo()->internalOptions->internal->siteAnalysis->reset();
}
return new \WP_REST_Response( [
'success' => false,
'response' => $responseBody[ $analyzeOrHomeUrl ]
], 400 );
}
if ( $analyzeUrl ) {
$results = $responseBody[ $analyzeOrHomeUrl ]['results'];
SeoAnalyzerResult::addResults( [
'results' => $results,
'score' => $responseBody[ $analyzeOrHomeUrl ]['score']
], $analyzeUrl );
$result = SeoAnalyzerResult::getCompetitorsResults();
return new \WP_REST_Response( $result, 200 );
}
$results = $responseBody[ $analyzeOrHomeUrl ]['results'];
SeoAnalyzerResult::addResults( [
'results' => $results,
'score' => $responseBody[ $analyzeOrHomeUrl ]['score']
] );
return new \WP_REST_Response( $responseBody[ $analyzeOrHomeUrl ], 200 );
}
/**
* Deletes the analyzed site for SEO.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function deleteSite( $request ) {
$body = $request->get_json_params();
$analyzeUrl = ! empty( $body['url'] ) ? esc_url_raw( urldecode( $body['url'] ) ) : null;
SeoAnalyzerResult::deleteByUrl( $analyzeUrl );
$competitors = aioseo()->internalOptions->internal->siteAnalysis->competitors;
unset( $competitors[ $analyzeUrl ] );
// Reset the competitors.
aioseo()->internalOptions->internal->siteAnalysis->competitors = $competitors;
return new \WP_REST_Response( $competitors, 200 );
}
/**
* Analyzes the title for SEO.
*
* @since 4.1.2
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function analyzeHeadline( $request ) {
$body = $request->get_json_params();
$headline = ! empty( $body['headline'] ) ? sanitize_text_field( $body['headline'] ) : '';
$shouldStoreHeadline = ! empty( $body['shouldStoreHeadline'] ) ? rest_sanitize_boolean( $body['shouldStoreHeadline'] ) : false;
if ( empty( $headline ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => __( 'Please enter a valid headline.', 'all-in-one-seo-pack' )
], 400 );
}
$result = aioseo()->standalone->headlineAnalyzer->getResult( $headline );
if ( ! $result['analysed'] ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $result['result']->msg
], 400 );
}
$headlines = aioseo()->internalOptions->internal->headlineAnalysis->headlines;
$headlines = array_reverse( $headlines, true );
// Remove a headline from the list if it already exists.
// This will ensure the new analysis is the first and open/highlighted.
if ( array_key_exists( $headline, $headlines ) ) {
unset( $headlines[ $headline ] );
}
$headlines[ $headline ] = wp_json_encode( $result );
$headlines = array_reverse( $headlines, true );
// Store the headlines with the latest one.
if ( $shouldStoreHeadline ) {
aioseo()->internalOptions->internal->headlineAnalysis->headlines = $headlines;
}
return new \WP_REST_Response( $headlines, 200 );
}
/**
* Deletes the analyzed Headline for SEO.
*
* @since 4.1.6
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function deleteHeadline( $request ) {
$body = $request->get_json_params();
$headline = sanitize_text_field( $body['headline'] );
$headlines = aioseo()->internalOptions->internal->headlineAnalysis->headlines;
unset( $headlines[ $headline ] );
// Reset the headlines.
aioseo()->internalOptions->internal->headlineAnalysis->headlines = $headlines;
return new \WP_REST_Response( $headlines, 200 );
}
/**
* Get Homepage results.
*
* @since 4.8.3
*
* @return \WP_REST_Response The response.
*/
public static function getHomeResults() {
$results = SeoAnalyzerResult::getResults();
return new \WP_REST_Response( [
'success' => true,
'result' => $results,
], 200 );
}
/**
* Get competitors results.
*
* @since 4.8.3
*
* @return \WP_REST_Response The response.
*/
public static function getCompetitorsResults() {
$results = SeoAnalyzerResult::getCompetitorsResults();
return new \WP_REST_Response( [
'success' => true,
'result' => $results,
], 200 );
}
} Api/Api.php 0000666 00000050051 15113050716 0006503 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Api class for the admin.
*
* @since 4.0.0
*/
class Api {
/**
* The REST API Namespace
*
* @since 4.0.0
*
* @var string
*/
public $namespace = 'aioseo/v1';
/**
* The routes we use in the rest API.
*
* @since 4.0.0
*
* @var array
*/
protected $routes = [
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
'GET' => [
'options' => [ 'callback' => [ 'Settings', 'getOptions' ], 'access' => 'everyone' ],
'ping' => [ 'callback' => [ 'Ping', 'ping' ], 'access' => 'everyone' ],
'post' => [ 'callback' => [ 'PostsTerms', 'getPostData' ], 'access' => 'everyone' ],
'post/(?P<postId>[\d]+)/first-attached-image' => [ 'callback' => [ 'PostsTerms', 'getFirstAttachedImage' ], 'access' => 'aioseo_page_social_settings' ],
'user/(?P<userId>[\d]+)/image' => [ 'callback' => [ 'User', 'getUserImage' ], 'access' => 'aioseo_page_social_settings' ],
'tags' => [ 'callback' => [ 'Tags', 'getTags' ], 'access' => 'everyone' ],
'search-statistics/url/auth' => [ 'callback' => [ 'SearchStatistics', 'getAuthUrl' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings', 'aioseo_setup_wizard' ] ], // phpcs:ignore Generic.Files.LineLength.MaxExceeded
'search-statistics/url/reauth' => [ 'callback' => [ 'SearchStatistics', 'getReauthUrl' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings' ] ],
'writing-assistant/keyword/(?P<postId>[\d]+)' => [ 'callback' => [ 'WritingAssistant', 'getPostKeyword' ], 'access' => 'aioseo_page_writing_assistant_settings' ],
'writing-assistant/user-info' => [ 'callback' => [ 'WritingAssistant', 'getUserInfo' ], 'access' => 'aioseo_page_writing_assistant_settings' ],
'writing-assistant/user-options' => [ 'callback' => [ 'WritingAssistant', 'getUserOptions' ], 'access' => 'aioseo_page_writing_assistant_settings' ],
'writing-assistant/report-history' => [ 'callback' => [ 'WritingAssistant', 'getReportHistory' ], 'access' => 'aioseo_page_writing_assistant_settings' ],
'seo-analysis/homeresults' => [ 'callback' => [ 'Analyze', 'getHomeResults' ], 'access' => 'aioseo_seo_analysis_settings' ],
'seo-analysis/competitors' => [ 'callback' => [ 'Analyze', 'getCompetitorsResults' ], 'access' => 'aioseo_seo_analysis_settings' ]
],
'POST' => [
'htaccess' => [ 'callback' => [ 'Tools', 'saveHtaccess' ], 'access' => 'aioseo_tools_settings' ],
'post' => [
'callback' => [ 'PostsTerms', 'updatePosts' ],
'access' => [
'aioseo_page_analysis',
'aioseo_page_general_settings',
'aioseo_page_advanced_settings',
'aioseo_page_schema_settings',
'aioseo_page_social_settings'
]
],
'post/(?P<postId>[\d]+)/disable-primary-term-education' => [ 'callback' => [ 'PostsTerms', 'disablePrimaryTermEducation' ], 'access' => 'aioseo_page_general_settings' ],
'post/(?P<postId>[\d]+)/disable-link-format-education' => [ 'callback' => [ 'PostsTerms', 'disableLinkFormatEducation' ], 'access' => 'aioseo_page_general_settings' ],
'post/(?P<postId>[\d]+)/update-internal-link-count' => [ 'callback' => [ 'PostsTerms', 'updateInternalLinkCount' ], 'access' => 'aioseo_page_general_settings' ],
'post/(?P<postId>[\d]+)/process-content' => [ 'callback' => [ 'PostsTerms', 'processContent' ], 'access' => 'aioseo_page_general_settings' ],
'posts-list/load-details-column' => [ 'callback' => [ 'PostsTerms', 'loadPostDetailsColumn' ], 'access' => 'aioseo_page_general_settings' ],
'posts-list/update-details-column' => [ 'callback' => [ 'PostsTerms', 'updatePostDetailsColumn' ], 'access' => 'aioseo_page_general_settings' ],
'terms-list/load-details-column' => [ 'callback' => [ 'PostsTerms', 'loadTermDetailsColumn' ], 'access' => 'aioseo_page_general_settings' ],
'terms-list/update-details-column' => [ 'callback' => [ 'PostsTerms', 'updateTermDetailsColumn' ], 'access' => 'aioseo_page_general_settings' ],
'keyphrases' => [ 'callback' => [ 'PostsTerms', 'updatePostKeyphrases' ], 'access' => 'aioseo_page_analysis' ],
'analyze' => [ 'callback' => [ 'Analyze', 'analyzeSite' ], 'access' => 'aioseo_seo_analysis_settings' ],
'analyze-headline' => [ 'callback' => [ 'Analyze', 'analyzeHeadline' ], 'access' => 'everyone' ],
'analyze-headline/delete' => [ 'callback' => [ 'Analyze', 'deleteHeadline' ], 'access' => 'aioseo_seo_analysis_settings' ],
'analyze/delete-site' => [ 'callback' => [ 'Analyze', 'deleteSite' ], 'access' => 'aioseo_seo_analysis_settings' ],
'clear-log' => [ 'callback' => [ 'Tools', 'clearLog' ], 'access' => 'aioseo_tools_settings' ],
'connect' => [ 'callback' => [ 'Connect', 'saveConnectToken' ], 'access' => [ 'aioseo_general_settings', 'aioseo_setup_wizard' ] ],
'connect-pro' => [ 'callback' => [ 'Connect', 'processConnect' ], 'access' => [ 'aioseo_general_settings', 'aioseo_setup_wizard' ] ],
'connect-url' => [ 'callback' => [ 'Connect', 'getConnectUrl' ], 'access' => [ 'aioseo_general_settings', 'aioseo_setup_wizard' ] ],
'backup' => [ 'callback' => [ 'Tools', 'createBackup' ], 'access' => 'aioseo_tools_settings' ],
'backup/restore' => [ 'callback' => [ 'Tools', 'restoreBackup' ], 'access' => 'aioseo_tools_settings' ],
'email-debug-info' => [ 'callback' => [ 'Tools', 'emailDebugInfo' ], 'access' => 'aioseo_tools_settings' ],
'migration/fix-blank-formats' => [ 'callback' => [ 'Migration', 'fixBlankFormats' ], 'access' => 'any' ],
'notification/blog-visibility-reminder' => [ 'callback' => [ 'Notifications', 'blogVisibilityReminder' ], 'access' => 'any' ],
'notification/conflicting-plugins-reminder' => [ 'callback' => [ 'Notifications', 'conflictingPluginsReminder' ], 'access' => 'any' ],
'notification/description-format-reminder' => [ 'callback' => [ 'Notifications', 'descriptionFormatReminder' ], 'access' => 'any' ],
'notification/email-reports-enable' => [ 'callback' => [ 'EmailSummary', 'enableEmailReports' ], 'access' => 'any' ],
'notification/install-addons-reminder' => [ 'callback' => [ 'Notifications', 'installAddonsReminder' ], 'access' => 'any' ],
'notification/install-aioseo-image-seo-reminder' => [ 'callback' => [ 'Notifications', 'installImageSeoReminder' ], 'access' => 'any' ],
'notification/install-aioseo-local-business-reminder' => [ 'callback' => [ 'Notifications', 'installLocalBusinessReminder' ], 'access' => 'any' ],
'notification/install-aioseo-news-sitemap-reminder' => [ 'callback' => [ 'Notifications', 'installNewsSitemapReminder' ], 'access' => 'any' ],
'notification/install-aioseo-video-sitemap-reminder' => [ 'callback' => [ 'Notifications', 'installVideoSitemapReminder' ], 'access' => 'any' ],
'notification/install-mi-reminder' => [ 'callback' => [ 'Notifications', 'installMiReminder' ], 'access' => 'any' ],
'notification/install-om-reminder' => [ 'callback' => [ 'Notifications', 'installOmReminder' ], 'access' => 'any' ],
'notification/v3-migration-custom-field-reminder' => [ 'callback' => [ 'Notifications', 'migrationCustomFieldReminder' ], 'access' => 'any' ],
'notification/v3-migration-schema-number-reminder' => [ 'callback' => [ 'Notifications', 'migrationSchemaNumberReminder' ], 'access' => 'any' ],
'notifications/dismiss' => [ 'callback' => [ 'Notifications', 'dismissNotifications' ], 'access' => 'any' ],
'objects' => [ 'callback' => [ 'PostsTerms', 'searchForObjects' ], 'access' => [ 'aioseo_search_appearance_settings', 'aioseo_sitemap_settings' ] ], // phpcs:ignore Generic.Files.LineLength.MaxExceeded
'options' => [ 'callback' => [ 'Settings', 'saveChanges' ], 'access' => 'any' ],
'plugins/deactivate' => [ 'callback' => [ 'Plugins', 'deactivatePlugins' ], 'access' => 'aioseo_feature_manager_settings' ],
'plugins/install' => [ 'callback' => [ 'Plugins', 'installPlugins' ], 'access' => [ 'install_plugins', 'aioseo_feature_manager_settings' ] ],
'plugins/upgrade' => [ 'callback' => [ 'Plugins', 'upgradePlugins' ], 'access' => [ 'update_plugins', 'aioseo_feature_manager_settings' ] ],
'reset-settings' => [ 'callback' => [ 'Settings', 'resetSettings' ], 'access' => 'aioseo_tools_settings' ],
'search-statistics/sitemap/delete' => [ 'callback' => [ 'SearchStatistics', 'deleteSitemap' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings' ] ], // phpcs:ignore Generic.Files.LineLength.MaxExceeded
'search-statistics/sitemap/ignore' => [ 'callback' => [ 'SearchStatistics', 'ignoreSitemap' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings' ] ], // phpcs:ignore Generic.Files.LineLength.MaxExceeded
'settings/export' => [ 'callback' => [ 'Settings', 'exportSettings' ], 'access' => 'aioseo_tools_settings' ],
'settings/export-content' => [ 'callback' => [ 'Settings', 'exportContent' ], 'access' => 'aioseo_tools_settings' ],
'settings/hide-setup-wizard' => [ 'callback' => [ 'Settings', 'hideSetupWizard' ], 'access' => 'any' ],
'settings/hide-upgrade-bar' => [ 'callback' => [ 'Settings', 'hideUpgradeBar' ], 'access' => 'any' ],
'settings/import' => [ 'callback' => [ 'Settings', 'importSettings' ], 'access' => 'aioseo_tools_settings' ],
'settings/import/(?P<siteId>[\d]+)' => [ 'callback' => [ 'Settings', 'importSettings' ], 'access' => 'aioseo_tools_settings' ],
'settings/import-plugins' => [ 'callback' => [ 'Settings', 'importPlugins' ], 'access' => 'aioseo_tools_settings' ],
'settings/toggle-card' => [ 'callback' => [ 'Settings', 'toggleCard' ], 'access' => 'any' ],
'settings/toggle-radio' => [ 'callback' => [ 'Settings', 'toggleRadio' ], 'access' => 'any' ],
'settings/dismiss-alert' => [ 'callback' => [ 'Settings', 'dismissAlert' ], 'access' => 'any' ],
'settings/items-per-page' => [ 'callback' => [ 'Settings', 'changeItemsPerPage' ], 'access' => 'any' ],
'settings/semrush-country' => [ 'callback' => [ 'Settings', 'changeSemrushCountry' ], 'access' => 'any' ],
'settings/do-task' => [ 'callback' => [ 'Settings', 'doTask' ], 'access' => 'aioseo_tools_settings' ],
'sitemap/deactivate-conflicting-plugins' => [ 'callback' => [ 'Sitemaps', 'deactivateConflictingPlugins' ], 'access' => 'any' ],
'sitemap/delete-static-files' => [ 'callback' => [ 'Sitemaps', 'deleteStaticFiles' ], 'access' => 'aioseo_sitemap_settings' ],
'sitemap/validate-html-sitemap-slug' => [ 'callback' => [ 'Sitemaps', 'validateHtmlSitemapSlug' ], 'access' => 'aioseo_sitemap_settings' ],
'tools/delete-robots-txt' => [ 'callback' => [ 'Tools', 'deleteRobotsTxt' ], 'access' => 'aioseo_tools_settings' ],
'tools/import-robots-txt' => [ 'callback' => [ 'Tools', 'importRobotsTxt' ], 'access' => 'aioseo_tools_settings' ],
'wizard' => [ 'callback' => [ 'Wizard', 'saveWizard' ], 'access' => 'aioseo_setup_wizard' ],
'integration/semrush/authenticate' => [
'callback' => [ 'Semrush', 'semrushAuthenticate', 'AIOSEO\\Plugin\\Common\\Api\\Integrations' ],
'access' => 'aioseo_page_analysis'
],
'integration/semrush/refresh' => [
'callback' => [ 'Semrush', 'semrushRefresh', 'AIOSEO\\Plugin\\Common\\Api\\Integrations' ],
'access' => 'aioseo_page_analysis'
],
'integration/semrush/keyphrases' => [
'callback' => [ 'Semrush', 'semrushGetKeyphrases', 'AIOSEO\\Plugin\\Common\\Api\\Integrations' ],
'access' => 'aioseo_page_analysis'
],
'integration/wpcode/snippets' => [
'callback' => [ 'WpCode', 'getSnippets', 'AIOSEO\\Plugin\\Common\\Api\\Integrations' ],
'access' => 'aioseo_tools_settings'
],
'crawl-cleanup' => [
'callback' => [ 'CrawlCleanup', 'fetchLogs', 'AIOSEO\\Plugin\\Common\\QueryArgs' ],
'access' => 'aioseo_search_appearance_settings'
],
'crawl-cleanup/block' => [
'callback' => [ 'CrawlCleanup', 'blockArg', 'AIOSEO\\Plugin\\Common\\QueryArgs' ],
'access' => 'aioseo_search_appearance_settings'
],
'crawl-cleanup/delete-blocked' => [
'callback' => [ 'CrawlCleanup', 'deleteBlocked', 'AIOSEO\\Plugin\\Common\\QueryArgs' ],
'access' => 'aioseo_search_appearance_settings'
],
'crawl-cleanup/delete-unblocked' => [
'callback' => [ 'CrawlCleanup', 'deleteLog', 'AIOSEO\\Plugin\\Common\\QueryArgs' ],
'access' => 'aioseo_search_appearance_settings'
],
'email-summary/send' => [
'callback' => [ 'EmailSummary', 'send' ],
'access' => 'aioseo_page_advanced_settings'
],
'writing-assistant/process' => [
'callback' => [ 'WritingAssistant', 'processKeyword' ],
'access' => 'aioseo_page_writing_assistant_settings'
],
'writing-assistant/content-analysis' => [
'callback' => [ 'WritingAssistant', 'getContentAnalysis' ],
'access' => 'aioseo_page_writing_assistant_settings'
],
'writing-assistant/disconnect' => [
'callback' => [ 'WritingAssistant', 'disconnect' ],
'access' => 'aioseo_page_writing_assistant_settings'
],
'writing-assistant/user-options' => [
'callback' => [ 'WritingAssistant', 'saveUserOptions' ],
'access' => 'aioseo_page_writing_assistant_settings'
],
'writing-assistant/set-report-progress' => [
'callback' => [ 'WritingAssistant', 'setReportProgress' ],
'access' => 'aioseo_page_writing_assistant_settings'
]
],
'DELETE' => [
'backup' => [ 'callback' => [ 'Tools', 'deleteBackup' ], 'access' => 'aioseo_tools_settings' ],
'search-statistics/auth' => [ 'callback' => [ 'SearchStatistics', 'deleteAuth' ], 'access' => [ 'aioseo_search_statistics_settings', 'aioseo_general_settings' ] ]
]
// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
];
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
add_filter( 'rest_allowed_cors_headers', [ $this, 'allowedHeaders' ] );
add_action( 'rest_api_init', [ $this, 'registerRoutes' ] );
}
/**
* Get all the routes to register.
*
* @since 4.0.0
*
* @return array An array of routes.
*/
protected function getRoutes() {
return $this->routes;
}
/**
* Registers the API routes.
*
* @since 4.0.0
*
* @return void
*/
public function registerRoutes() {
$class = new \ReflectionClass( get_called_class() );
foreach ( $this->getRoutes() as $method => $data ) {
foreach ( $data as $route => $options ) {
register_rest_route(
$this->namespace,
$route,
[
'methods' => $method,
'permission_callback' => empty( $options['permissions'] ) ? [ $this, 'validRequest' ] : [ $this, $options['permissions'] ],
'callback' => is_array( $options['callback'] )
? [
(
! empty( $options['callback'][2] )
? $options['callback'][2] . '\\' . $options['callback'][0]
: (
class_exists( $class->getNamespaceName() . '\\' . $options['callback'][0] )
? $class->getNamespaceName() . '\\' . $options['callback'][0]
: __NAMESPACE__ . '\\' . $options['callback'][0]
)
),
$options['callback'][1]
]
: [ $this, $options['callback'] ]
]
);
}
}
}
/**
* Sets headers that are allowed for our API routes.
*
* @since 4.0.0
*
* @return void
*/
public function allowHeaders() {
// TODO: Remove this entire function after a while. It's only here to ensure compatibility with people that are still using Image SEO 1.0.3 or lower.
header( 'Access-Control-Allow-Headers: X-WP-Nonce' );
}
/**
* Sets headers that are allowed for our API routes.
*
* @since 4.1.1
*
* @param array $allowHeaders The allowed request headers.
* @return array $allowHeaders The allowed request headers.
*/
public function allowedHeaders( $allowHeaders ) {
if ( ! array_search( 'X-WP-Nonce', $allowHeaders, true ) ) {
$allowHeaders[] = 'X-WP-Nonce';
}
return $allowHeaders;
}
/**
* Determine if logged in or has the proper permissions.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request.
* @return bool True if validated, false if not.
*/
public function validRequest( $request ) {
return is_user_logged_in() && $this->validateAccess( $request );
}
/**
* Validates access from the routes array.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request.
* @return bool True if validated, false if not.
*/
public function validateAccess( $request ) {
$routeData = $this->getRouteData( $request );
if ( empty( $routeData ) || empty( $routeData['access'] ) ) {
return false;
}
// Admins always have access.
if ( aioseo()->access->isAdmin() ) {
return true;
}
switch ( $routeData['access'] ) {
case 'everyone':
// Any user is able to access the route.
return true;
default:
return aioseo()->access->hasCapability( $routeData['access'] );
}
}
/**
* Returns the data for the route that is being accessed.
*
* @since 4.1.6
*
* @param \WP_REST_Request $request The REST Request.
* @return array The route data.
*/
protected function getRouteData( $request ) {
// NOTE: Since WordPress uses case-insensitive patterns to match routes,
// we are forcing everything to lowercase to ensure we have the proper route.
// This prevents users with lower privileges from accessing routes they shouldn't.
$route = aioseo()->helpers->toLowercase( $request->get_route() );
$route = untrailingslashit( str_replace( '/' . $this->namespace . '/', '', $route ) );
$routeData = isset( $this->getRoutes()[ $request->get_method() ][ $route ] ) ? $this->getRoutes()[ $request->get_method() ][ $route ] : [];
// No direct route name, let's try the regexes.
if ( empty( $routeData ) ) {
foreach ( $this->getRoutes()[ $request->get_method() ] as $routeRegex => $routeInfo ) {
$routeRegex = str_replace( '@', '\@', $routeRegex );
if ( preg_match( "@{$routeRegex}@", (string) $route ) ) {
$routeData = $routeInfo;
break;
}
}
}
return $routeData;
}
} Api/PostsTerms.php 0000666 00000034521 15113050716 0010121 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Route class for the API.
*
* @since 4.0.0
*/
class PostsTerms {
/**
* Searches for posts or terms by ID/name.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function searchForObjects( $request ) {
$body = $request->get_json_params();
if ( empty( $body['query'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No search term was provided.'
], 400 );
}
if ( empty( $body['type'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No type was provided.'
], 400 );
}
$searchQuery = esc_sql( aioseo()->core->db->db->esc_like( $body['query'] ) );
$objects = [];
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
if ( 'posts' === $body['type'] ) {
$postTypes = aioseo()->helpers->getPublicPostTypes( true );
foreach ( $postTypes as $postType ) {
// Check if post type isn't noindexed.
if ( $dynamicOptions->searchAppearance->postTypes->has( $postType ) && ! $dynamicOptions->searchAppearance->postTypes->$postType->show ) {
$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
}
}
$objects = aioseo()->core->db
->start( 'posts' )
->select( 'ID, post_type, post_title, post_name' )
->whereRaw( "( post_title LIKE '%{$searchQuery}%' OR post_name LIKE '%{$searchQuery}%' OR ID = '{$searchQuery}' )" )
->whereIn( 'post_type', $postTypes )
->whereIn( 'post_status', [ 'publish', 'draft', 'future', 'pending' ] )
->orderBy( 'post_title' )
->limit( 10 )
->run()
->result();
} elseif ( 'terms' === $body['type'] ) {
$taxonomies = aioseo()->helpers->getPublicTaxonomies( true );
foreach ( $taxonomies as $taxonomy ) {
// Check if taxonomy isn't noindexed.
if ( $dynamicOptions->searchAppearance->taxonomies->has( $taxonomy ) && ! $dynamicOptions->searchAppearance->taxonomies->$taxonomy->show ) {
$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
}
}
$objects = aioseo()->core->db
->start( 'terms as t' )
->select( 't.term_id as term_id, t.slug as slug, t.name as name, tt.taxonomy as taxonomy' )
->join( 'term_taxonomy as tt', 't.term_id = tt.term_id', 'INNER' )
->whereRaw( "( t.name LIKE '%{$searchQuery}%' OR t.slug LIKE '%{$searchQuery}%' OR t.term_id = '{$searchQuery}' )" )
->whereIn( 'tt.taxonomy', $taxonomies )
->orderBy( 't.name' )
->limit( 10 )
->run()
->result();
}
if ( empty( $objects ) ) {
return new \WP_REST_Response( [
'success' => true,
'objects' => []
], 200 );
}
$parsed = [];
foreach ( $objects as $object ) {
if ( 'posts' === $body['type'] ) {
$parsed[] = [
'type' => $object->post_type,
'value' => (int) $object->ID,
'slug' => $object->post_name,
'label' => $object->post_title,
'link' => get_permalink( $object->ID )
];
} elseif ( 'terms' === $body['type'] ) {
$parsed[] = [
'type' => $object->taxonomy,
'value' => (int) $object->term_id,
'slug' => $object->slug,
'label' => $object->name,
'link' => get_term_link( $object->term_id )
];
}
}
return new \WP_REST_Response( [
'success' => true,
'objects' => $parsed
], 200 );
}
/**
* Get post data for fetch requests
*
* @since 4.0.0
* @version 4.8.3 Changes the return value to include only the Vue data.
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function getPostData( $request ) {
$args = $request->get_query_params();
if ( empty( $args['postId'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No post ID was provided.'
], 400 );
}
return new \WP_REST_Response( [
'success' => true,
'data' => aioseo()->helpers->getVueData( 'post', $args['postId'], $args['integrationSlug'] ?? null )
], 200 );
}
/**
* Get the first attached image for a post.
*
* @since 4.1.8
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function getFirstAttachedImage( $request ) {
$args = $request->get_params();
if ( empty( $args['postId'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No post ID was provided.'
], 400 );
}
// Disable the cache.
aioseo()->social->image->useCache = false;
$post = aioseo()->helpers->getPost( $args['postId'] );
$url = aioseo()->social->image->getImage( 'facebook', 'attach', $post );
// Reset the cache property.
aioseo()->social->image->useCache = true;
return new \WP_REST_Response( [
'success' => true,
'url' => is_array( $url ) ? $url[0] : $url,
], 200 );
}
/**
* Returns the posts custom fields.
*
* @since 4.0.6
*
* @param \WP_Post|int $post The post.
* @return string The custom field content.
*/
private static function getAnalysisContent( $post = null ) {
$analysisContent = apply_filters( 'aioseo_analysis_content', aioseo()->helpers->getPostContent( $post ) );
return sanitize_post_field( 'post_content', $analysisContent, $post->ID, 'display' );
}
/**
* Update post settings.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function updatePosts( $request ) {
$body = $request->get_json_params();
$postId = ! empty( $body['id'] ) ? intval( $body['id'] ) : null;
if ( ! $postId ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Post ID is missing.'
], 400 );
}
$body['id'] = $postId;
$body['title'] = ! empty( $body['title'] ) ? sanitize_text_field( $body['title'] ) : null;
$body['description'] = ! empty( $body['description'] ) ? sanitize_text_field( $body['description'] ) : null;
$body['keywords'] = ! empty( $body['keywords'] ) ? aioseo()->helpers->sanitize( $body['keywords'] ) : null;
$body['og_title'] = ! empty( $body['og_title'] ) ? sanitize_text_field( $body['og_title'] ) : null;
$body['og_description'] = ! empty( $body['og_description'] ) ? sanitize_text_field( $body['og_description'] ) : null;
$body['og_article_section'] = ! empty( $body['og_article_section'] ) ? sanitize_text_field( $body['og_article_section'] ) : null;
$body['og_article_tags'] = ! empty( $body['og_article_tags'] ) ? aioseo()->helpers->sanitize( $body['og_article_tags'] ) : null;
$body['twitter_title'] = ! empty( $body['twitter_title'] ) ? sanitize_text_field( $body['twitter_title'] ) : null;
$body['twitter_description'] = ! empty( $body['twitter_description'] ) ? sanitize_text_field( $body['twitter_description'] ) : null;
$error = Models\Post::savePost( $postId, $body );
if ( ! empty( $error ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Failed update query: ' . $error
], 401 );
}
return new \WP_REST_Response( [
'success' => true,
'posts' => $postId
], 200 );
}
/**
* Load post settings from Post screen.
*
* @since 4.5.5
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function loadPostDetailsColumn( $request ) {
$body = $request->get_json_params();
$ids = ! empty( $body['ids'] ) ? (array) $body['ids'] : [];
if ( ! $ids ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Post IDs are missing.'
], 400 );
}
$posts = [];
foreach ( $ids as $postId ) {
$postTitle = get_the_title( $postId );
$headline = ! empty( $postTitle ) ? sanitize_text_field( $postTitle ) : ''; // We need this to achieve consistency for the score when using special characters in titles
$headlineResult = aioseo()->standalone->headlineAnalyzer->getResult( $headline );
$posts[] = [
'id' => $postId,
'titleParsed' => aioseo()->meta->title->getPostTitle( $postId ),
'descriptionParsed' => aioseo()->meta->description->getPostDescription( $postId ),
'headlineScore' => ! empty( $headlineResult['score'] ) ? (int) $headlineResult['score'] : 0,
];
}
return new \WP_REST_Response( [
'success' => true,
'posts' => $posts
], 200 );
}
/**
* Update post settings from Post screen.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function updatePostDetailsColumn( $request ) {
$body = $request->get_json_params();
$postId = ! empty( $body['postId'] ) ? intval( $body['postId'] ) : null;
$isMedia = isset( $body['isMedia'] ) ? true : false;
if ( ! $postId ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Post ID is missing.'
], 400 );
}
$aioseoPost = Models\Post::getPost( $postId );
$aioseoData = json_decode( wp_json_encode( $aioseoPost ), true );
if ( $isMedia ) {
wp_update_post(
[
'ID' => $postId,
'post_title' => sanitize_text_field( $body['imageTitle'] ),
]
);
update_post_meta( $postId, '_wp_attachment_image_alt', sanitize_text_field( $body['imageAltTag'] ) );
}
Models\Post::savePost( $postId, array_replace( $aioseoData, $body ) );
$lastError = aioseo()->core->db->lastError();
if ( ! empty( $lastError ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Failed update query: ' . $lastError
], 401 );
}
return new \WP_REST_Response( [
'success' => true,
'posts' => $postId,
'title' => aioseo()->meta->title->getPostTitle( $postId ),
'description' => aioseo()->meta->description->getPostDescription( $postId )
], 200 );
}
/**
* Update post keyphrases.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function updatePostKeyphrases( $request ) {
$body = $request->get_json_params();
$postId = ! empty( $body['postId'] ) ? intval( $body['postId'] ) : null;
if ( ! $postId ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Post ID is missing.'
], 400 );
}
$thePost = Models\Post::getPost( $postId );
$thePost->post_id = $postId;
if ( ! empty( $body['keyphrases'] ) ) {
$thePost->keyphrases = wp_json_encode( $body['keyphrases'] );
}
if ( ! empty( $body['page_analysis'] ) ) {
$thePost->page_analysis = wp_json_encode( $body['page_analysis'] );
}
if ( ! empty( $body['seo_score'] ) ) {
$thePost->seo_score = sanitize_text_field( $body['seo_score'] );
}
$thePost->updated = gmdate( 'Y-m-d H:i:s' );
$thePost->save();
$lastError = aioseo()->core->db->lastError();
if ( ! empty( $lastError ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Failed update query: ' . $lastError
], 401 );
}
return new \WP_REST_Response( [
'success' => true,
'post' => $postId
], 200 );
}
/**
* Disable the Primary Term education.
*
* @since 4.3.6
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function disablePrimaryTermEducation( $request ) {
$args = $request->get_params();
if ( empty( $args['postId'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No post ID was provided.'
], 400 );
}
$thePost = Models\Post::getPost( $args['postId'] );
$thePost->options->primaryTerm->productEducationDismissed = true;
$thePost->save();
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Disable the link format education.
*
* @since 4.2.2
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function disableLinkFormatEducation( $request ) {
$args = $request->get_params();
if ( empty( $args['postId'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No post ID was provided.'
], 400 );
}
$thePost = Models\Post::getPost( $args['postId'] );
$thePost->options->linkFormat->linkAssistantDismissed = true;
$thePost->save();
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Increment the internal link count.
*
* @since 4.2.2
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function updateInternalLinkCount( $request ) {
$args = $request->get_params();
$body = $request->get_json_params();
$count = ! empty( $body['count'] ) ? intval( $body['count'] ) : null;
if ( empty( $args['postId'] ) || null === $count ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No post ID or count was provided.'
], 400 );
}
$thePost = Models\Post::getPost( $args['postId'] );
$thePost->options->linkFormat->internalLinkCount = $count;
$thePost->save();
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Get the processed content by the given raw content.
*
* @since 4.5.2
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function processContent( $request ) {
$args = $request->get_params();
$body = $request->get_json_params();
if ( empty( $args['postId'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No post ID was provided.'
], 400 );
}
// Check if we can process it using a page builder integration.
$pageBuilder = aioseo()->helpers->getPostPageBuilderName( $args['postId'] );
if ( ! empty( $pageBuilder ) ) {
return new \WP_REST_Response( [
'success' => true,
'content' => aioseo()->standalone->pageBuilderIntegrations[ $pageBuilder ]->processContent( $args['postId'], $body['content'] ),
], 200 );
}
// Check if the content was passed, otherwise get it from the post.
$content = $body['content'] ?? aioseo()->helpers->getPostContent( $args['postId'] );
return new \WP_REST_Response( [
'success' => true,
'content' => apply_filters( 'the_content', $content ),
], 200 );
}
} Api/Sitemaps.php 0000666 00000011452 15113050716 0007561 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Route class for the API.
*
* @since 4.0.0
*/
class Sitemaps {
/**
* Delete all static sitemap files.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function deleteStaticFiles() {
require_once ABSPATH . 'wp-admin/includes/file.php';
$files = list_files( get_home_path(), 1 );
if ( ! count( $files ) ) {
return;
}
$isGeneralSitemapStatic = aioseo()->options->sitemap->general->advancedSettings->enable &&
in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) &&
! aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic;
$detectedFiles = [];
if ( ! $isGeneralSitemapStatic ) {
foreach ( $files as $filename ) {
if ( preg_match( '#.*sitemap.*#', (string) $filename ) ) {
// We don't want to delete the video sitemap here at all.
$isVideoSitemap = preg_match( '#.*video.*#', (string) $filename ) ? true : false;
if ( ! $isVideoSitemap ) {
$detectedFiles[] = $filename;
}
}
}
}
if ( ! count( $detectedFiles ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No sitemap files found.'
], 400 );
}
$fs = aioseo()->core->fs;
if ( ! $fs->isWpfsValid() ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No access to filesystem.'
], 400 );
}
foreach ( $detectedFiles as $file ) {
$fs->fs->delete( $file, false, 'f' );
}
Models\Notification::deleteNotificationByName( 'sitemap-static-files' );
return new \WP_REST_Response( [
'success' => true,
'notifications' => Models\Notification::getNotifications()
], 200 );
}
/**
* Deactivates conflicting plugins.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function deactivateConflictingPlugins() {
$error = esc_html__( 'Deactivation failed. Please check permissions and try again.', 'all-in-one-seo-pack' );
if ( ! current_user_can( 'install_plugins' ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $error
], 400 );
}
aioseo()->conflictingPlugins->deactivateConflictingPlugins( [ 'seo', 'sitemap' ] );
Models\Notification::deleteNotificationByName( 'conflicting-plugins' );
return new \WP_REST_Response( [
'success' => true,
'notifications' => Models\Notification::getNotifications()
], 200 );
}
/**
* Check whether the slug for the HTML sitemap is not in use.
*
* @since 4.1.3
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function validateHtmlSitemapSlug( $request ) {
$body = $request->get_json_params();
$pageUrl = ! empty( $body['pageUrl'] ) ? sanitize_text_field( $body['pageUrl'] ) : '';
if ( empty( $pageUrl ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No path was provided.'
], 400 );
}
$parsedPageUrl = wp_parse_url( $pageUrl );
if ( empty( $parsedPageUrl['path'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'The given path is invalid.'
], 400 );
}
$isUrl = aioseo()->helpers->isUrl( $pageUrl );
$isInternalUrl = aioseo()->helpers->isInternalUrl( $pageUrl );
if ( $isUrl && ! $isInternalUrl ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'The given URL is not a valid internal URL.'
], 400 );
}
$pathExists = self::pathExists( $parsedPageUrl['path'], false );
return new \WP_REST_Response( [
'exists' => $pathExists
], 200 );
}
/**
* Checks whether the given path is unique or not.
*
* @since 4.1.4
* @version 4.2.6
*
* @param string $path The path.
* @param bool $path Whether the given path is a URL.
* @return boolean Whether the path exists.
*/
private static function pathExists( $path, $isUrl ) {
$path = trim( aioseo()->helpers->excludeHomePath( $path ), '/' );
$url = $isUrl ? $path : trailingslashit( home_url() ) . $path;
$url = user_trailingslashit( $url );
// Let's do another check here, just to be sure that the domain matches.
if ( ! aioseo()->helpers->isInternalUrl( $url ) ) {
return false;
}
$response = wp_safe_remote_head( $url );
$status = wp_remote_retrieve_response_code( $response );
if ( ! $status ) {
// If there is no status code, we might be in a local environment with CURL misconfigured.
// In that case we can still check if a post exists for the path by quering the DB.
$post = aioseo()->helpers->getPostbyPath(
$path,
OBJECT,
aioseo()->helpers->getPublicPostTypes( true )
);
return is_object( $post );
}
return 200 === $status;
}
} Api/WritingAssistant.php 0000666 00000021163 15113050716 0011311 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* WritingAssistant class for the API.
*
* @since 4.7.4
*/
class WritingAssistant {
/**
* Process the keyword.
*
* @since 4.7.4
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function processKeyword( $request ) {
$body = $request->get_json_params();
$postId = absint( $body['postId'] );
$keywordText = sanitize_text_field( $body['keyword'] );
$country = sanitize_text_field( $body['country'] );
$language = sanitize_text_field( strtolower( $body['language'] ) );
if ( empty( $keywordText ) || empty( $country ) || empty( $language ) ) {
return new \WP_REST_Response( [
'success' => false,
'error' => __( 'Missing data to generate a report', 'all-in-one-seo-pack' )
] );
}
$keyword = Models\WritingAssistantKeyword::getKeyword( $keywordText, $country, $language );
$writingAssistantPost = Models\WritingAssistantPost::getPost( $postId );
if ( $keyword->exists() ) {
$writingAssistantPost->attachKeyword( $keyword->id );
// Returning early will let the UI code start polling the keyword.
return new \WP_REST_Response( [
'success' => true,
'progress' => $keyword->progress
], 200 );
}
// Start a new keyword process.
$processResult = aioseo()->writingAssistant->seoBoost->service->processKeyword( $keywordText, $country, $language );
if ( is_wp_error( $processResult ) ) {
return new \WP_REST_Response( [
'success' => false,
'error' => $processResult->get_error_message()
] );
}
// Store the new keyword.
$keyword->uuid = $processResult['slug'];
$keyword->progress = 0;
$keyword->save();
// Update the writing assistant post with the current keyword.
$writingAssistantPost->attachKeyword( $keyword->id );
return new \WP_REST_Response( [ 'success' => true ], 200 );
}
/**
* Get current keyword for a Post.
*
* @since 4.7.4
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function getPostKeyword( $request ) {
$postId = $request->get_param( 'postId' );
if ( empty( $postId ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => __( 'Empty Post ID', 'all-in-one-seo-pack' )
], 404 );
}
$keyword = Models\WritingAssistantPost::getKeyword( $postId );
if ( $keyword && 100 !== $keyword->progress ) {
// Update progress.
$newProgress = aioseo()->writingAssistant->seoBoost->service->getProgressAndResult( $keyword->uuid );
if ( is_wp_error( $newProgress ) ) {
return new \WP_REST_Response( [
'success' => false,
'error' => $newProgress->get_error_message()
], 200 );
}
if ( 'success' !== $newProgress['status'] ) {
return new \WP_REST_Response( [
'success' => false,
'error' => $newProgress['msg']
], 200 );
}
$keyword->progress = ! empty( $newProgress['report']['progress'] ) ? $newProgress['report']['progress'] : $keyword->progress;
if ( ! empty( $newProgress['report']['keywords'] ) ) {
$keyword->keywords = $newProgress['report']['keywords'];
}
if ( ! empty( $newProgress['report']['competitors'] ) ) {
$keyword->competitors = [
'competitors' => $newProgress['report']['competitors'],
'summary' => $newProgress['report']['competitors_summary']
];
}
$keyword->save();
}
// Return a refreshed keyword here because we need some parsed data.
$keyword = Models\WritingAssistantPost::getKeyword( $postId );
return new \WP_REST_Response( $keyword, 200 );
}
/**
* Get the content analysis for a post.
*
* @since 4.7.4
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function getContentAnalysis( $request ) {
$title = $request->get_param( 'title' );
$description = $request->get_param( 'description' );
$content = apply_filters( 'the_content', $request->get_param( 'content' ) );
$postId = $request->get_param( 'postId' );
if ( empty( $content ) || empty( $postId ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => __( 'Empty Content or Post ID', 'all-in-one-seo-pack' )
], 200 );
}
$keyword = Models\WritingAssistantPost::getKeyword( $postId );
if (
! $keyword ||
! $keyword->exists() ||
100 !== $keyword->progress
) {
return new \WP_REST_Response( [
'success' => false,
'error' => __( 'Keyword not found or not ready', 'all-in-one-seo-pack' )
], 200 );
}
$writingAssistantPost = Models\WritingAssistantPost::getPost( $postId );
// Make sure we're not analysing the same content again.
$contentHash = sha1( $content );
if (
! empty( $writingAssistantPost->content_analysis ) &&
$writingAssistantPost->content_analysis_hash === $contentHash
) {
return new \WP_REST_Response( $writingAssistantPost->content_analysis, 200 );
}
// Call SEOBoost service to get the content analysis.
$contentAnalysis = aioseo()->writingAssistant->seoBoost->service->getContentAnalysis( $title, $description, $content, $keyword->uuid );
if ( is_wp_error( $contentAnalysis ) ) {
return new \WP_REST_Response( [
'success' => false,
'error' => $contentAnalysis->get_error_message()
], 200 );
}
if ( empty( $contentAnalysis['result'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'error' => __( 'Empty response from service', 'all-in-one-seo-pack' )
], 200 );
}
// Update the post with the content analysis.
$writingAssistantPost->content_analysis = $contentAnalysis['result'];
$writingAssistantPost->content_analysis_hash = $contentHash;
$writingAssistantPost->save();
return new \WP_REST_Response( $contentAnalysis['result'], 200 );
}
/**
* Get the user info.
*
* @since 4.7.4
*
* @return \WP_REST_Response The response.
*/
public static function getUserInfo() {
$userInfo = aioseo()->writingAssistant->seoBoost->service->getUserInfo();
if ( is_wp_error( $userInfo ) ) {
return new \WP_REST_Response( [
'success' => false,
'error' => $userInfo->get_error_message()
], 200 );
}
if ( empty( $userInfo['status'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'error' => __( 'Empty response from service', 'all-in-one-seo-pack' )
], 200 );
}
if ( 'success' !== $userInfo['status'] ) {
return new \WP_REST_Response( [
'success' => false,
'error' => $userInfo['msg']
], 200 );
}
return new \WP_REST_Response( $userInfo, 200 );
}
/**
* Get the user info.
*
* @since 4.7.4
*
* @return \WP_REST_Response The response.
*/
public static function getUserOptions() {
$userOptions = aioseo()->writingAssistant->seoBoost->getUserOptions();
return new \WP_REST_Response( $userOptions, 200 );
}
/**
* Get the report history.
*
* @since 4.7.4
*
* @return \WP_REST_Response The response.
*/
public static function getReportHistory() {
$reportHistory = aioseo()->writingAssistant->seoBoost->getReportHistory();
if ( is_wp_error( $reportHistory ) ) {
return new \WP_REST_Response( [
'success' => false,
'error' => $reportHistory->get_error_message()
], 200 );
}
return new \WP_REST_Response( $reportHistory, 200 );
}
/**
* Disconnect the user.
*
* @since 4.7.4
*
* @return \WP_REST_Response The response.
*/
public static function disconnect() {
aioseo()->writingAssistant->seoBoost->setAccessToken( '' );
return new \WP_REST_Response( [ 'success' => true ], 200 );
}
/**
* Save user options.
*
* @since 4.7.4
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function saveUserOptions( $request ) {
$body = $request->get_json_params();
$userOptions = [
'country' => $body['country'],
'language' => $body['language'],
];
aioseo()->writingAssistant->seoBoost->setUserOptions( $userOptions );
return new \WP_REST_Response( [ 'success' => true ], 200 );
}
/**
* Set the report progress.
*
* @since 4.7.4
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function setReportProgress( $request ) {
$body = $request->get_json_params();
$keyword = Models\WritingAssistantPost::getKeyword( (int) $body['postId'] );
$keyword->progress = (int) $body['progress'];
$keyword->save();
return new \WP_REST_Response( [ 'success' => true ], 200 );
}
} Api/User.php 0000666 00000001402 15113050716 0006704 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles user related API routes.
*
* @since 4.2.8
*/
class User {
/**
* Get the user image.
*
* @since 4.2.8
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function getUserImage( $request ) {
$args = $request->get_params();
if ( empty( $args['userId'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No user ID was provided.'
], 400 );
}
$url = get_avatar_url( $args['userId'] );
return new \WP_REST_Response( [
'success' => true,
'url' => is_array( $url ) ? $url[0] : $url,
], 200 );
}
} Api/Tools.php 0000666 00000015430 15113050716 0007074 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Tools as CommonTools;
/**
* Route class for the API.
*
* @since 4.0.0
*/
class Tools {
/**
* Import contents from a robots.txt url, static file or pasted text.
*
* @since 4.0.0
* @version 4.4.2
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function importRobotsTxt( $request ) {
$body = $request->get_json_params();
$blogId = ! empty( $body['blogId'] ) ? $body['blogId'] : 0;
$source = ! empty( $body['source'] ) ? $body['source'] : '';
$text = ! empty( $body['text'] ) ? sanitize_textarea_field( $body['text'] ) : '';
$url = ! empty( $body['url'] ) ? sanitize_url( $body['url'], [ 'http', 'https' ] ) : '';
try {
if ( is_multisite() && 'network' !== $blogId ) {
aioseo()->helpers->switchToBlog( $blogId );
}
switch ( $source ) {
case 'url':
aioseo()->robotsTxt->importRobotsTxtFromUrl( $url, $blogId );
break;
case 'text':
aioseo()->robotsTxt->importRobotsTxtFromText( $text, $blogId );
break;
case 'static':
default:
aioseo()->robotsTxt->importPhysicalRobotsTxt( $blogId );
aioseo()->robotsTxt->deletePhysicalRobotsTxt();
$options = aioseo()->options;
if ( 'network' === $blogId ) {
$options = aioseo()->networkOptions;
}
$options->tools->robots->enable = true;
break;
}
return new \WP_REST_Response( [
'success' => true,
'notifications' => Models\Notification::getNotifications()
], 200 );
} catch ( \Exception $e ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $e->getMessage()
], 400 );
}
}
/**
* Delete the static robots.txt file.
*
* @since 4.0.0
* @version 4.4.5
*
* @return \WP_REST_Response The response.
*/
public static function deleteRobotsTxt() {
try {
aioseo()->robotsTxt->deletePhysicalRobotsTxt();
return new \WP_REST_Response( [
'success' => true,
'notifications' => Models\Notification::getNotifications()
], 200 );
} catch ( \Exception $e ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $e->getMessage()
], 400 );
}
}
/**
* Email debug info.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function emailDebugInfo( $request ) {
$body = $request->get_json_params();
$email = ! empty( $body['email'] ) ? $body['email'] : null;
if ( ! filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'invalid-email-address'
], 400 );
}
require_once ABSPATH . 'wp-admin/includes/update.php';
// Translators: 1 - The plugin name ("All in One SEO"), 2 - The Site URL.
$html = sprintf( __( '%1$s Debug Info from %2$s', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_NAME, aioseo()->helpers->getSiteDomain() ) . "\r\n------------------\r\n\r\n";
$info = CommonTools\SystemStatus::getSystemStatusInfo();
foreach ( $info as $group ) {
if ( empty( $group['results'] ) ) {
continue;
}
$html .= "\r\n\r\n{$group['label']}\r\n";
foreach ( $group['results'] as $data ) {
$html .= "{$data['header']}: {$data['value']}\r\n";
}
}
if ( ! wp_mail(
$email,
// Translators: 1 - The plugin name ("All in One SEO).
sprintf( __( '%1$s Debug Info', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_NAME ),
$html
) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Unable to send debug email, please check your email send settings and try again.'
], 400 );
}
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Create a settings backup.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function createBackup( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
aioseo()->backup->create();
return new \WP_REST_Response( [
'success' => true,
'backups' => array_reverse( aioseo()->backup->all() )
], 200 );
}
/**
* Restore a settings backup.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function restoreBackup( $request ) {
$body = $request->get_json_params();
$backup = ! empty( $body['backup'] ) ? (int) $body['backup'] : null;
if ( empty( $backup ) ) {
return new \WP_REST_Response( [
'success' => false,
'backups' => array_reverse( aioseo()->backup->all() )
], 400 );
}
aioseo()->backup->restore( $backup );
return new \WP_REST_Response( [
'success' => true,
'backups' => array_reverse( aioseo()->backup->all() ),
'options' => aioseo()->options->all(),
'internalOptions' => aioseo()->internalOptions->all()
], 200 );
}
/**
* Delete a settings backup.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function deleteBackup( $request ) {
$body = $request->get_json_params();
$backup = ! empty( $body['backup'] ) ? (int) $body['backup'] : null;
if ( empty( $backup ) ) {
return new \WP_REST_Response( [
'success' => false,
'backups' => array_reverse( aioseo()->backup->all() )
], 400 );
}
aioseo()->backup->delete( $backup );
return new \WP_REST_Response( [
'success' => true,
'backups' => array_reverse( aioseo()->backup->all() )
], 200 );
}
/**
* Save the .htaccess file.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function saveHtaccess( $request ) {
$body = $request->get_json_params();
$htaccess = ! empty( $body['htaccess'] ) ? sanitize_textarea_field( $body['htaccess'] ) : '';
if ( empty( $htaccess ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => __( '.htaccess file is empty.', 'all-in-one-seo-pack' )
], 400 );
}
$htaccess = aioseo()->helpers->decodeHtmlEntities( $htaccess );
$saveHtaccess = (object) aioseo()->htaccess->saveContents( $htaccess );
if ( ! $saveHtaccess->success ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $saveHtaccess->message ? $saveHtaccess->message : __( 'An error occurred while trying to write to the .htaccess file. Please try again later.', 'all-in-one-seo-pack' ),
'reason' => $saveHtaccess->reason
], 400 );
}
return new \WP_REST_Response( [
'success' => true
], 200 );
}
} Api/EmailSummary.php 0000666 00000003027 15113050716 0010400 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Email Summary related REST API endpoint callbacks.
*
* @since 4.7.2
*/
class EmailSummary {
/**
* Sends a summary.
*
* @since 4.7.2
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function send( $request ) {
try {
$body = $request->get_json_params();
$to = $body['to'] ?? '';
$frequency = $body['frequency'] ?? '';
if ( $to && $frequency ) {
aioseo()->emailReports->summary->run( [
'recipient' => $to,
'frequency' => $frequency,
] );
}
return new \WP_REST_Response( [
'success' => true,
], 200 );
} catch ( \Exception $e ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $e->getMessage()
], 200 );
}
}
/**
* Enable email reports from notification.
*
* @since 4.7.7
*
* @return \WP_REST_Response The response.
*/
public static function enableEmailReports() {
// Update option.
aioseo()->options->advanced->emailSummary->enable = true;
// Remove notification.
$notification = Models\Notification::getNotificationByName( 'email-reports-enable-reminder' );
if ( $notification->exists() ) {
$notification->delete();
}
// Send a success response.
return new \WP_REST_Response( [
'success' => true,
'notifications' => Models\Notification::getNotifications()
], 200 );
}
} Api/Connect.php 0000666 00000004650 15113050716 0007367 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Route class for the API.
*
* @since 4.0.0
*/
class Connect {
/**
* Get the connect URL.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function getConnectUrl( $request ) {
$body = $request->get_json_params();
$key = ! empty( $body['licenseKey'] ) ? sanitize_text_field( $body['licenseKey'] ) : null;
$wizard = ! empty( $body['wizard'] ) ? (bool) $body['wizard'] : false;
$success = true;
$urlData = aioseo()->admin->connect->generateConnectUrl( $key, $wizard ? admin_url( 'index.php?page=aioseo-setup-wizard#/success' ) : null );
$url = '';
$message = '';
if ( ! empty( $urlData['error'] ) ) {
$success = false;
$message = $urlData['error'];
}
$url = $urlData['url'];
return new \WP_REST_Response( [
'success' => $success,
'url' => $url,
'message' => $message,
'popup' => ! isset( $urlData['popup'] ) ? true : $urlData['popup']
], 200 );
}
/**
* Process the connection.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function processConnect( $request ) {
$body = $request->get_json_params();
$wizard = ! empty( $body['wizard'] ) ? sanitize_text_field( $body['wizard'] ) : null;
$success = true;
if ( $wizard ) {
aioseo()->internalOptions->internal->wizard = $wizard;
}
$response = aioseo()->admin->connect->process();
if ( ! empty( $response['error'] ) ) {
$message = $response['error'];
} else {
$message = $response['success'];
}
return new \WP_REST_Response( [
'success' => $success,
'message' => $message
], 200 );
}
/**
* Saves the connect token.
*
* @since 4.0.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function saveConnectToken( $request ) {
$body = $request->get_json_params();
$token = ! empty( $body['token'] ) ? sanitize_text_field( $body['token'] ) : null;
$success = true;
$message = 'token-saved';
aioseo()->internalOptions->internal->siteAnalysis->connectToken = $token;
return new \WP_REST_Response( [
'success' => $success,
'message' => $message
], 200 );
}
} Api/SearchStatistics.php 0000666 00000013703 15113050716 0011255 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\SearchStatistics\Api;
/**
* Route class for the API.
*
* @since 4.3.0
* @version 4.6.2 Moved from Pro to Common.
*/
class SearchStatistics {
/**
* Get the authorize URL.
*
* @since 4.3.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function getAuthUrl( $request ) {
$body = $request->get_params();
if ( aioseo()->searchStatistics->api->auth->isConnected() ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Cannot authenticate. Please re-authenticate.'
], 200 );
}
$returnTo = ! empty( $body['returnTo'] ) ? sanitize_key( $body['returnTo'] ) : '';
$url = add_query_arg( [
'tt' => aioseo()->searchStatistics->api->trustToken->get(),
'sitei' => aioseo()->searchStatistics->api->getSiteIdentifier(),
'version' => aioseo()->version,
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'siteurl' => site_url(),
'return' => urlencode( admin_url( 'admin.php?page=aioseo&return-to=' . $returnTo ) ),
'testurl' => 'https://' . aioseo()->searchStatistics->api->getApiUrl() . '/v1/test/'
], 'https://' . aioseo()->searchStatistics->api->getApiUrl() . '/v1/auth/new/' . aioseo()->searchStatistics->api->auth->type . '/' );
$url = apply_filters( 'aioseo_search_statistics_auth_url', $url );
return new \WP_REST_Response( [
'success' => true,
'url' => $url,
], 200 );
}
/**
* Get the reauthorize URL.
*
* @since 4.3.0
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function getReauthUrl( $request ) {
$body = $request->get_params();
if ( ! aioseo()->searchStatistics->api->auth->isConnected() ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Cannot re-authenticate. Please authenticate.',
], 200 );
}
$returnTo = ! empty( $body['returnTo'] ) ? sanitize_key( $body['returnTo'] ) : '';
$url = add_query_arg( [
'tt' => aioseo()->searchStatistics->api->trustToken->get(),
'sitei' => aioseo()->searchStatistics->api->getSiteIdentifier(),
'version' => aioseo()->version,
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'siteurl' => site_url(),
'key' => aioseo()->searchStatistics->api->auth->getKey(),
'token' => aioseo()->searchStatistics->api->auth->getToken(),
'return' => urlencode( admin_url( 'admin.php?page=aioseo&return-to=' . $returnTo ) ),
'testurl' => 'https://' . aioseo()->searchStatistics->api->getApiUrl() . '/v1/test/'
], 'https://' . aioseo()->searchStatistics->api->getApiUrl() . '/v1/auth/reauth/' . aioseo()->searchStatistics->api->auth->type . '/' );
$url = apply_filters( 'aioseo_search_statistics_reauth_url', $url );
return new \WP_REST_Response( [
'success' => true,
'url' => $url,
], 200 );
}
/**
* Delete the authorization.
*
* @since 4.3.0
*
* @return \WP_REST_Response The response.
*/
public static function deleteAuth() {
if ( ! aioseo()->searchStatistics->api->auth->isConnected() ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Cannot deauthenticate. You are not currently authenticated.'
], 200 );
}
aioseo()->searchStatistics->api->auth->delete();
aioseo()->searchStatistics->cancelActions();
return new \WP_REST_Response( [
'success' => true,
'message' => 'Successfully deauthenticated.'
], 200 );
}
/**
* Deletes a sitemap.
*
* @since 4.6.2
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function deleteSitemap( $request ) {
$body = $request->get_json_params();
$sitemap = ! empty( $body['sitemap'] ) ? $body['sitemap'] : '';
if ( empty( $sitemap ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No sitemap provided.'
], 200 );
}
$args = [
'sitemap' => $sitemap
];
$api = new Api\Request( 'google-search-console/sitemap/delete/', $args, 'POST' );
$response = $api->request();
if ( is_wp_error( $response ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $response['message']
], 200 );
}
aioseo()->internalOptions->searchStatistics->sitemap->list = $response['data'];
aioseo()->internalOptions->searchStatistics->sitemap->lastFetch = time();
return new \WP_REST_Response( [
'success' => true,
'data' => [
'internalOptions' => aioseo()->internalOptions->searchStatistics->sitemap->all(),
'sitemapsWithErrors' => aioseo()->searchStatistics->sitemap->getSitemapsWithErrors()
]
], 200 );
}
/**
* Ignores a sitemap.
*
* @since 4.6.2
*
* @param \WP_REST_Request $request The REST Request
* @return \WP_REST_Response The response.
*/
public static function ignoreSitemap( $request ) {
$body = $request->get_json_params();
$sitemap = ! empty( $body['sitemap'] ) ? $body['sitemap'] : '';
if ( empty( $sitemap ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'No sitemap provided.'
], 200 );
}
$ignoredSitemaps = aioseo()->internalOptions->searchStatistics->sitemap->ignored;
if ( is_array( $sitemap ) ) {
$ignoredSitemaps = array_merge( $ignoredSitemaps, $sitemap );
} else {
$ignoredSitemaps[] = $sitemap;
}
$ignoredSitemaps = array_unique( $ignoredSitemaps ); // Remove duplicates.
$ignoredSitemaps = array_filter( $ignoredSitemaps ); // Remove empty values.
aioseo()->internalOptions->searchStatistics->sitemap->ignored = $ignoredSitemaps;
return new \WP_REST_Response( [
'success' => true,
'data' => [
'internalOptions' => aioseo()->internalOptions->searchStatistics->sitemap->all(),
'sitemapsWithErrors' => aioseo()->searchStatistics->sitemap->getSitemapsWithErrors()
]
], 200 );
}
} Api/Ping.php 0000666 00000000631 15113050716 0006666 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Route class for the API.
*
* @since 4.0.0
*/
class Ping {
/**
* Returns a success if the API is alive.
*
* @since 4.0.0
*
* @return \WP_REST_Response The response.
*/
public static function ping() {
return new \WP_REST_Response( [
'success' => true
], 200 );
}
} Models/Model.php 0000666 00000025156 15113050716 0007554 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Models;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Base Model class.
*
* @since 4.0.0
*/
#[\AllowDynamicProperties]
class Model implements \JsonSerializable {
/**
* Fields that can be null when saving to the database.
*
* @since 4.0.0
*
* @var array
*/
protected $nullFields = [];
/**
* Fields that should be encoded/decoded on save/get.
*
* @since 4.0.0
*
* @var array
*/
protected $jsonFields = [];
/**
* Fields that should be boolean values.
*
* @since 4.0.0
*
* @var array
*/
protected $booleanFields = [];
/**
* Fields that should be integer values.
*
* @since 4.1.0
* @version 4.7.3 Renamed from numericFields to integerFields.
*
* @var array
*/
protected $integerFields = [];
/**
* Fields that should be float values.
*
* @since 4.7.3
*
* @var array
*/
protected $floatFields = [];
/**
* Fields that should be hidden when serialized.
*
* @since 4.0.0
*
* @var array
*/
protected $hidden = [];
/**
* The table used in the SQL query.
*
* @since 4.0.0
*
* @var string
*/
protected $table = '';
/**
* The primary key retrieved from the table.
*
* @since 4.0.0
*
* @var string
*/
protected $pk = 'id';
/**
* The ID of the model.
* This needs to be null in order for MySQL to auto-increment correctly if the NO_AUTO_VALUE_ON_ZERO SQL mode is enabled.
*
* @since 4.2.7
*
* @var int|null
*/
public $id = null;
/**
* An array of columns from the DB that we can use.
*
* @since 4.0.0
*
* @var array
*/
private static $columns = [];
/**
* Class constructor.
*
* @since 4.0.0
*
* @param mixed $var This can be the primary key of the resource, or it could be an array of data to manufacture a resource without a database query.
*/
public function __construct( $var = null ) {
$skip = [ 'id', 'created', 'updated' ];
$fields = [];
foreach ( $this->getColumns() as $column => $value ) {
if ( ! in_array( $column, $skip, true ) ) {
$fields[ $column ] = $value;
}
}
$this->applyKeys( $fields );
// Process straight through if we were given a valid array.
if ( is_array( $var ) || is_object( $var ) ) {
// Apply keys to object.
$this->applyKeys( $var );
if ( $this->exists() ) {
return true;
}
return false;
}
return $this->loadData( $var );
}
/**
* Load the data from the database!
*
* @since 4.0.0
*
* @param mixed $var Generally the primary key to load up the model from the DB.
* @return Model|bool Returns the current object.
*/
protected function loadData( $var = null ) {
// Return false if var is invalid or not supplied.
if ( null === $var ) {
return false;
}
$query = aioseo()->core->db
->start( $this->table )
->where( $this->pk, $var )
->limit( 1 )
->output( 'ARRAY_A' );
$result = $query->run();
if ( ! $result || $result->nullSet() ) {
return $this;
}
// Apply keys to object.
$this->applyKeys( $result->result()[0] );
return $this;
}
/**
* Take the keys from the result array and add them to the Model.
*
* @since 4.0.0
*
* @param array $array The array of keys and values to add to the Model.
* @return void
*/
protected function applyKeys( $array ) {
if ( ! is_object( $array ) && ! is_array( $array ) ) {
throw new \Exception( '$array must either be an object or an array.' );
}
foreach ( (array) $array as $key => $value ) {
$key = trim( $key );
$this->$key = $value;
if ( null === $value && in_array( $key, $this->nullFields, true ) ) {
continue;
}
if ( in_array( $key, $this->jsonFields, true ) ) {
if ( $value ) {
$this->$key = is_string( $value ) ? json_decode( $value ) : $value;
}
continue;
}
if ( in_array( $key, $this->booleanFields, true ) ) {
$this->$key = (bool) $value;
continue;
}
if ( in_array( $key, $this->integerFields, true ) ) {
$this->$key = (int) $value;
continue;
}
if ( in_array( $key, $this->floatFields, true ) ) {
$this->$key = (float) $value;
continue;
}
}
}
/**
* Let's filter out any properties we cannot save to the database.
*
* @since 4.0.0
*
* @param array $keys The array of keys to filter.
* @return array The array of valid columns for the database query.
*/
protected function filter( $keys ) {
$fields = [];
$skip = [ 'created', 'updated' ];
$dbColumns = aioseo()->db->getColumns( $this->table );
foreach ( $dbColumns as $column ) {
if ( ! in_array( $column, $skip, true ) && array_key_exists( $column, $keys ) ) {
$fields[ $column ] = $keys[ $column ];
}
}
return $fields;
}
/**
* Transforms the data to be null if it exists in the nullFields variables.
*
* @since 4.0.0
*
* @param array $data The data array to transform.
* @return array The transformed data.
*/
protected function transform( $data, $set = false ) {
foreach ( $this->nullFields as $field ) {
if ( isset( $data[ $field ] ) && empty( $data[ $field ] ) ) {
// Because sitemap prio can both be 0 and null, we need to make sure it's 0 if it's set.
if ( 'priority' === $field && 0.0 === $data[ $field ] ) {
continue;
}
$data[ $field ] = null;
}
}
foreach ( $this->booleanFields as $field ) {
if ( isset( $data[ $field ] ) && '' === $data[ $field ] ) {
unset( $data[ $field ] );
} elseif ( isset( $data[ $field ] ) ) {
$data[ $field ] = (bool) $data[ $field ] ? 1 : 0;
}
}
if ( $set ) {
return $data;
}
foreach ( $this->integerFields as $field ) {
if ( isset( $data[ $field ] ) ) {
$data[ $field ] = (int) $data[ $field ];
}
}
foreach ( $this->jsonFields as $field ) {
if ( isset( $data[ $field ] ) && ! aioseo()->helpers->isJsonString( $data[ $field ] ) ) {
if ( is_array( $data[ $field ] ) && aioseo()->helpers->isArrayNumeric( $data[ $field ] ) ) {
$data[ $field ] = array_values( $data[ $field ] );
}
$data[ $field ] = wp_json_encode( $data[ $field ] );
}
}
return $data;
}
/**
* Sets a piece of data to the requested resource.
*
* @since 4.0.0
*/
public function set() {
$args = func_get_args();
$count = func_num_args();
if ( ! is_array( $args[0] ) && $count < 2 ) {
throw new \Exception( 'The set method must contain at least 2 arguments: key and value. Or an array of data. Only one argument was passed and it was not an array.' );
}
$key = $args[0];
$value = ! empty( $args[1] ) ? $args[1] : null;
// Make sure we have a key.
if ( false === $key ) {
return false;
}
// If it's not an array, make it one.
if ( ! is_array( $key ) ) {
$key = [ $key => $value ];
}
// Preprocess data.
$key = $this->transform( $key, true );
// Save the items in this object.
foreach ( $key as $k => $v ) {
if ( ! empty( $k ) ) {
$this->$k = $v;
}
}
}
/**
* Delete the Model Resource itself.
*
* @since 4.0.0
*
* @return null
*/
public function delete() {
if ( ! $this->exists() ) {
return;
}
aioseo()->core->db
->delete( $this->table )
->where( $this->pk, $this->id )
->run();
return null;
}
/**
* Saves data to the requested resource.
*
* @since 4.0.0
*/
public function save() {
$fields = $this->transform( $this->filter( (array) get_object_vars( $this ) ) );
$id = null;
if ( count( $fields ) > 0 ) {
$pk = $this->pk;
if ( isset( $this->$pk ) && '' !== $this->$pk ) {
// PK specified.
$pkv = $this->$pk;
$query = aioseo()->core->db
->start( $this->table )
->where( [ $pk => $pkv ] )
->resetCache()
->run();
if ( ! $query->nullSet() ) {
// Row exists in database.
$fields['updated'] = gmdate( 'Y-m-d H:i:s' );
aioseo()->core->db
->update( $this->table )
->set( $fields )
->where( [ $pk => $pkv ] )
->run();
$id = $this->$pk;
} else {
// Row does not exist.
$fields[ $pk ] = $pkv;
$fields['created'] = gmdate( 'Y-m-d H:i:s' );
$fields['updated'] = gmdate( 'Y-m-d H:i:s' );
$id = aioseo()->core->db
->insert( $this->table )
->set( $fields )
->run()
->insertId();
if ( $id ) {
$this->$pk = $id;
}
}
} else {
$fields['created'] = gmdate( 'Y-m-d H:i:s' );
$fields['updated'] = gmdate( 'Y-m-d H:i:s' );
$id = aioseo()->core->db
->insert( $this->table )
->set( $fields )
->run()
->insertId();
if ( $id ) {
$this->$pk = $id;
}
}
}
// Refresh the resource.
$this->reset( $id );
}
/**
* Check if the model exists in the database.
*
* @since 4.0.0
*
* @return bool If the model exists, true otherwise false.
*/
public function exists() {
return ( ! empty( $this->{$this->pk} ) ) ? true : false;
}
/**
* Resets a resource by forcing internal updates to be applied.
*
* @since 4.0.0
*
* @param string $id The resource ID.
* @return void
*/
public function reset( $id = null ) {
$id = ! empty( $id ) ? $id : $this->{$this->pk};
$this->__construct( $id );
}
/**
* Helper function to remove data we don't want serialized.
*
* @since 4.0.0
*
* @return array An array of data that we are okay with serializing.
*/
#[\ReturnTypeWillChange]
// The attribute above omits a deprecation notice from PHP 8.1 that is thrown because the return type of jsonSerialize() isn't "mixed".
// Once PHP 7.x is our minimum supported version, this can be removed in favour of overriding the return type in the method signature like this -
// public function jsonSerialize() : array
public function jsonSerialize() {
$array = [];
foreach ( $this->getColumns() as $column => $value ) {
if ( in_array( $column, $this->hidden, true ) ) {
continue;
}
$array[ $column ] = isset( $this->$column ) ? $this->$column : null;
}
return $array;
}
/**
* Retrieves the columns for the model.
*
* @since 4.0.0
*
* @return array An array of columns.
*/
public function getColumns() {
if ( empty( self::$columns[ get_called_class() ] ) ) {
self::$columns[ get_called_class() ] = [];
// Let's set the columns that are available by default.
$table = aioseo()->core->db->prefix . $this->table;
$results = aioseo()->core->db->start( $table )
->output( 'OBJECT' )
->execute( 'SHOW COLUMNS FROM `' . $table . '`', true )
->result();
foreach ( $results as $col ) {
self::$columns[ get_called_class() ][ $col->Field ] = $col->Default;
}
if ( ! empty( $this->appends ) ) {
foreach ( $this->appends as $append ) {
self::$columns[ get_called_class() ][ $append ] = null;
}
}
}
return self::$columns[ get_called_class() ];
}
} Models/WritingAssistantPost.php 0000666 00000006734 15113050716 0012700 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Models;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Posts.
*
* @since 4.7.4
*/
class WritingAssistantPost extends Model {
/**
* The name of the table in the database, without the prefix.
*
* @since 4.7.4
*
* @var string
*/
protected $table = 'aioseo_writing_assistant_posts';
/**
* Fields that should be integer values.
*
* @since 4.7.4
*
* @var array
*/
protected $integerFields = [ 'id', 'post_id', 'keyword_id' ];
/**
* Fields that should be boolean values.
*
* @since 4.7.4
*
* @var array
*/
protected $booleanFields = [];
/**
* Fields that should be encoded/decoded on save/get.
*
* @since 4.7.4
*
* @var array
*/
protected $jsonFields = [ 'content_analysis' ];
/**
* Gets a post's content analysis.
*
* @since 4.7.4
*
* @param int $postId A post ID.
* @return array The post content's analysis.
*/
public static function getContentAnalysis( $postId ) {
$post = self::getPost( $postId );
return ! empty( $post->content_analysis ) && is_object( $post->content_analysis ) ? (array) $post->content_analysis : [];
}
/**
* Gets a writing assistant post.
*
* @since 4.7.4
*
* @param int $postId A post ID.
* @return WritingAssistantPost The post object.
*/
public static function getPost( $postId ) {
$post = aioseo()->core->db->start( 'aioseo_writing_assistant_posts' )
->where( 'post_id', $postId )
->run()
->model( 'AIOSEO\Plugin\Common\Models\WritingAssistantPost' );
if ( ! $post->exists() ) {
$post->post_id = $postId;
}
return $post;
}
/**
* Gets a post's current keyword.
*
* @since 4.7.4
*
* @param int $postId A post ID.
* @return WritingAssistantKeyword|bool An attached keyword.
*/
public static function getKeyword( $postId ) {
$post = self::getPost( $postId );
if ( ! $post->exists() || empty( $post->keyword_id ) ) {
return false;
}
$keyword = aioseo()->core->db->start( 'aioseo_writing_assistant_keywords' )
->where( 'id', $post->keyword_id )
->run()
->model( 'AIOSEO\Plugin\Common\Models\WritingAssistantKeyword' );
// This is here so this property is reactive in the frontend.
if ( ! empty( $keyword->keywords ) ) {
foreach ( $keyword->keywords as &$keyph ) {
$keyph->contentCount = 0;
}
}
// Help sorting in the frontend.
if ( ! empty( $keyword->competitors->competitors ) ) {
foreach ( $keyword->competitors->competitors as &$competitor ) {
$competitor->wasAnalyzed = true;
if ( 0 >= $competitor->wordCount ) {
$competitor->wordCount = 0;
$competitor->readabilityScore = 999;
$competitor->readabilityGrade = '';
$competitor->gradeScore = 0;
$competitor->grade = '';
$competitor->wasAnalyzed = false;
}
$competitor->readabilityScore = (float) $competitor->readabilityScore;
}
}
return $keyword;
}
/**
* Return if a post has a keyword.
*
* @since 4.7.4
*
* @param int $postId A post ID.
* @return boolean Has a keyword.
*/
public static function hasKeyword( $postId ) {
$post = self::getPost( $postId );
return (bool) $post->keyword_id;
}
/**
* Attaches a keyword to a post.
*
* @since 4.7.4
*
* @param int $keywordId The keyword ID.
* @return void
*/
public function attachKeyword( $keywordId ) {
$this->keyword_id = $keywordId;
$this->save();
}
} Models/CrawlCleanupBlockedArg.php 0000666 00000012252 15113050716 0013003 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Models;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models as CommonModels;
/**
* The Crawl Cleanup Blocked Arg DB Model.
*
* @since 4.5.8
*/
class CrawlCleanupBlockedArg extends CommonModels\Model {
/**
* The name of the table in the database, without the prefix.
*
* @since 4.5.8
*
* @var string
*/
protected $table = 'aioseo_crawl_cleanup_blocked_args';
/**
* Fields that should be hidden when serialized.
*
* @since 4.5.8
*
* @var array
*/
protected $hidden = [ 'id' ];
/**
* Fields that should be numeric values.
*
* @since 4.5.8
*
* @var array
*/
protected $integerFields = [ 'id', 'hits' ];
/**
* Field to count hits.
*
* @since 4.5.8
*
* @var integer
*/
protected $hits = 0;
/**
* Field for Regex.
*
* @since 4.5.8
*
* @var string
*/
public $regex = null;
/**
* Field that contains the hash for key+value
*
* @since 4.5.8
*
* @var string
*/
public $key_value_hash = null;
/**
* Separator used to merge key and value string.
*
* @since 4.5.8
*
* @var string
*/
private static $keyValueSeparator = '=';
/**
* Separator used to merge key and value string.
*
* @since 4.5.8
*
* @var CrawlCleanupBlockedArg|null
*/
private static $regexBlockedArgs = null;
/**
* Class constructor.
*
* @since 4.5.8
*
* @param mixed $var This can be the primary key of the resource, or it could be an array of data to manufacture a resource without a database query.
*/
public function __construct( $var = null ) {
parent::__construct( $var );
}
/**
* Get Blocked row using Key and Value.
*
* @since 4.5.8
*
* @param string $key The key to search.
* @param string $value The value to search.
* @return CrawlCleanupBlockedArg The CrawlCleanupBlockedArg object.
*/
public static function getByKeyValue( $key, $value ) {
$keyValue = self::getKeyValueString( $key, $value );
return aioseo()->core->db
->start( 'aioseo_crawl_cleanup_blocked_args' )
->where( 'key_value_hash', sha1( $keyValue ) )
->run()
->model( 'AIOSEO\\Plugin\\Common\\Models\\CrawlCleanupBlockedArg' );
}
/**
* Get Blocked row using Regex Value.
*
* @since 4.5.8
*
* @param string $regex The regex value to search.
* @return CrawlCleanupBlockedArg The CrawlCleanupBlockedArg object.
*/
public static function getByRegex( $regex ) {
return aioseo()->core->db
->start( 'aioseo_crawl_cleanup_blocked_args' )
->where( 'regex', $regex )
->run()
->model( 'AIOSEO\\Plugin\\Common\\Models\\CrawlCleanupBlockedArg' );
}
/**
* Look for regex match by key and value.
*
* @since 4.5.8
*
* @param string $key The key to search.
* @param string $value The value to search.
* @return CrawlCleanupBlockedArg The CrawlCleanupBlockedArg object.
*/
public static function matchRegex( $key, $value ) {
$keyValue = self::getKeyValueString( $key, $value );
$regexBlockedArgs = self::getRegexBlockedArgs();
foreach ( $regexBlockedArgs as $regexQueryArg ) {
$escapedRegex = str_replace( '@', '\@', $regexQueryArg->regex );
if ( preg_match( "@{$escapedRegex}@", (string) $keyValue ) ) {
return new CrawlCleanupBlockedArg( $regexQueryArg->id );
}
}
return new CrawlCleanupBlockedArg();
}
/**
* Get Regex rows.
*
* @since 4.5.8
*
* @return CrawlCleanupBlockedArg The CrawlCleanupBlockedArg object.
*/
public static function getRegexBlockedArgs() {
if ( null === self::$regexBlockedArgs ) {
self::$regexBlockedArgs = aioseo()->core->db
->start( 'aioseo_crawl_cleanup_blocked_args' )
->select( 'id, regex' )
->whereRaw( 'regex IS NOT NULL' )
->run()
->result();
}
return self::$regexBlockedArgs;
}
/**
* Transforms data as needed.
*
* @since 4.5.8
*
* @param array $data The data array to transform.
* @return array The transformed data.
*/
protected function transform( $data, $set = false ) {
$data = parent::transform( $data, $set );
// Create key+value hash.
if ( ! empty( $data['key'] ) ) {
$keyValue = self::getKeyValueString( $data['key'], $data['value'] );
$data['key_value_hash'] = sha1( $keyValue );
}
// Case hits number are empty start with 0.
if ( empty( $data['hits'] ) ) {
$data['hits'] = 0;
}
return $data;
}
/**
* Increase hits and save.
*
* @since 4.5.8
*
*/
public function addHit() {
if ( $this->id ) {
$this->hits++;
parent::save();
}
}
/**
* Return string with key and value with pattern model defined.
*
* @since 4.5.8
*
* @param string $key The key to merge.
* @param string $value The value to merge.
* @return string The result string merging key and value (case not empty).
*/
public static function getKeyValueString( $key, $value ) {
return $key . ( $value ? self::getKeyValueSeparator() . $value : '' );
}
/**
* Return string to separate key and value.
*
* @since 4.5.8
*
* @return string The separator for key and value.
*/
public static function getKeyValueSeparator() {
return self::$keyValueSeparator;
}
} Models/Post.php 0000666 00000076370 15113050716 0007445 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Models;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* The Post DB Model.
*
* @since 4.0.0
*/
class Post extends Model {
/**
* The name of the table in the database, without the prefix.
*
* @since 4.0.0
*
* @var string
*/
protected $table = 'aioseo_posts';
/**
* Fields that should be json encoded on save and decoded on get.
*
* @since 4.0.0
*
* @var array
*/
protected $jsonFields = [
'keywords',
'keyphrases',
'page_analysis',
'schema',
// 'schema_type_options',
'images',
'videos',
'open_ai',
'options',
'local_seo',
'primary_term',
'breadcrumb_settings',
'og_article_tags'
];
/**
* Fields that should be hidden when serialized.
*
* @since 4.0.0
*
* @var array
*/
protected $hidden = [ 'id' ];
/**
* Fields that should be boolean values.
*
* @since 4.0.13
*
* @var array
*/
protected $booleanFields = [
'twitter_use_og',
'pillar_content',
'robots_default',
'robots_noindex',
'robots_noarchive',
'robots_nosnippet',
'robots_nofollow',
'robots_noimageindex',
'robots_noodp',
'robots_notranslate',
'limit_modified_date',
];
/**
* Fields that can be null when saved.
*
* @since 4.5.7
*
* @var array
*/
protected $nullFields = [
'priority'
];
/**
* Fields that should be float values.
*
* @since 4.7.3
*
* @var array
*/
protected $floatFields = [
'priority'
];
/**
* Returns a Post with a given ID.
*
* @since 4.0.0
*
* @param int $postId The post ID.
* @return Post The Post object.
*/
public static function getPost( $postId ) {
// This is needed to prevent an error when upgrading from 4.1.8 to 4.1.9.
// WordPress deletes the attachment .zip file for the new plugin version after installing it, which triggers the "delete_post" hook.
// In-between the 4.1.8 to 4.1.9 update, the new Core class does not exist yet, causing the PHP error.
// TODO: Delete this in a future release.
$post = new self();
if ( ! property_exists( aioseo(), 'core' ) ) {
return $post;
}
$post = aioseo()->core->db->start( 'aioseo_posts' )
->where( 'post_id', $postId )
->run()
->model( 'AIOSEO\\Plugin\\Common\\Models\\Post' );
if ( ! $post->exists() ) {
$post->post_id = $postId;
$post = self::setDynamicDefaults( $post, $postId );
} else {
$post = self::runDynamicMigrations( $post );
}
// Set options object.
$post = self::setOptionsDefaults( $post );
return apply_filters( 'aioseo_get_post', $post );
}
/**
* Sets the dynamic defaults on the post object if it doesn't exist in the DB yet.
*
* @since 4.1.4
*
* @param Post $post The Post object.
* @param int $postId The post ID.
* @return Post The modified Post object.
*/
private static function setDynamicDefaults( $post, $postId ) {
if ( 'page' === get_post_type( $postId ) ) { // This check cannot be deleted and is required to prevent errors after WordPress cleans up the attachment it creates when a plugin is updated.
$isWooCommerceCheckoutPage = aioseo()->helpers->isWooCommerceCheckoutPage( $postId );
if (
$isWooCommerceCheckoutPage ||
aioseo()->helpers->isWooCommerceCartPage( $postId ) ||
aioseo()->helpers->isWooCommerceAccountPage( $postId )
) {
$post->robots_default = false;
$post->robots_noindex = true;
}
}
if ( aioseo()->helpers->isStaticHomePage( $postId ) ) {
$post->og_object_type = 'website';
}
$post->twitter_use_og = aioseo()->options->social->twitter->general->useOgData;
if ( property_exists( $post, 'schema' ) && null === $post->schema ) {
$post->schema = self::getDefaultSchemaOptions();
}
return $post;
}
/**
* Migrates removed QAPage schema on-the-fly when the post is loaded.
*
* @since 4.1.8
*
* @param Post $aioseoPost The post object.
* @return Post The modified post object.
*/
private static function migrateRemovedQaSchema( $aioseoPost ) {
if ( ! $aioseoPost->schema_type || 'webpage' !== strtolower( $aioseoPost->schema_type ) ) {
return $aioseoPost;
}
$schemaTypeOptions = json_decode( $aioseoPost->schema_type_options );
if ( 'qapage' !== strtolower( $schemaTypeOptions->webPage->webPageType ) ) {
return $aioseoPost;
}
$schemaTypeOptions->webPage->webPageType = 'WebPage';
$aioseoPost->schema_type_options = wp_json_encode( $schemaTypeOptions );
$aioseoPost->save();
return $aioseoPost;
}
/**
* Runs dynamic migrations whenever the post object is loaded.
*
* @since 4.1.7
*
* @param Post $post The Post object.
* @return Post The modified Post object.
*/
private static function runDynamicMigrations( $post ) {
$post = self::migrateRemovedQaSchema( $post );
$post = self::migrateImageTypes( $post );
$post = self::runDynamicSchemaMigration( $post );
$post = self::migrateKoreaCountryCodeSchemas( $post );
return $post;
}
/**
* Migrates the post's schema data when it is loaded.
*
* @since 4.2.5
*
* @param Post $post The Post object.
* @return Post The modified Post object.
*/
private static function runDynamicSchemaMigration( $post ) {
if ( ! property_exists( $post, 'schema' ) ) {
return $post;
}
if ( null === $post->schema ) {
$post = aioseo()->updates->migratePostSchemaHelper( $post );
}
// If the schema prop isn't set yet, we want to set it here.
// We also want to run this regardless of whether it is already set to make sure the default schema graph
// is correctly propagated on the frontend after changing it.
$post->schema = self::getDefaultSchemaOptions( $post->schema );
// Filter out null or empty graphs.
$post->schema->graphs = array_filter( $post->schema->graphs, function( $graph ) {
return ! empty( $graph );
} );
foreach ( $post->schema->graphs as $graph ) {
// If the first character of the graph ID isn't a pound, add one.
// We have to do this because the schema migration in 4.2.5 didn't add the pound for custom graphs.
if ( property_exists( $graph, 'id' ) && '#' !== substr( $graph->id, 0, 1 ) ) {
$graph->id = '#' . $graph->id;
}
// If the graph has an old rating value, we need to migrate it to the review.
if (
property_exists( $graph, 'id' ) &&
preg_match( '/(movie|software-application)/', (string) $graph->id ) &&
property_exists( $graph->properties, 'rating' ) &&
property_exists( $graph->properties->rating, 'value' )
) {
$graph->properties->review->rating = $graph->properties->rating->value;
unset( $graph->properties->rating->value );
}
// If the graph has audience data, we need to migrate it to the correct one.
if (
property_exists( $graph, 'id' ) &&
preg_match( '/(product|product-review)/', $graph->id ) &&
property_exists( $graph->properties, 'audience' )
) {
$graph->properties->audience = self::migratePostAudienceAgeSchema( $graph->properties->audience );
}
}
return $post;
}
/**
* Migrates the post's image types when it is loaded.
*
* @since 4.2.5
*
* @param Post $post The Post object.
* @return Post The modified Post object.
*/
private static function migrateImageTypes( $post ) {
$pageBuilder = aioseo()->helpers->getPostPageBuilderName( $post->post_id );
if ( ! $pageBuilder ) {
return $post;
}
$deprecatedImageSources = 'seedprod' === strtolower( $pageBuilder )
? [ 'auto', 'custom', 'featured' ]
: [ 'auto' ];
if ( ! empty( $post->og_image_type ) && in_array( $post->og_image_type, $deprecatedImageSources, true ) ) {
$post->og_image_type = 'default';
}
if ( ! empty( $post->twitter_image_type ) && in_array( $post->twitter_image_type, $deprecatedImageSources, true ) ) {
$post->twitter_image_type = 'default';
}
return $post;
}
/**
* Saves the Post object.
*
* @since 4.0.3
*
* @param int $postId The Post ID.
* @param array $data The post data to save.
* @return bool|void|string Whether the post data was saved or a DB error message.
*/
public static function savePost( $postId, $data ) {
if ( empty( $data ) ) {
return false;
}
$thePost = self::getPost( $postId );
$data = apply_filters( 'aioseo_save_post', $data, $thePost );
// Before setting the data, we check if the title/description are the same as the defaults and clear them if so.
$data = self::checkForDefaultFormat( $postId, $thePost, $data );
$thePost = self::sanitizeAndSetDefaults( $postId, $thePost, $data );
// Update traditional post meta so that it can be used by multilingual plugins.
self::updatePostMeta( $postId, $data );
$thePost->save();
$thePost->reset();
$lastError = aioseo()->core->db->lastError();
if ( ! empty( $lastError ) ) {
return $lastError;
}
// Fires once an AIOSEO post has been saved.
do_action( 'aioseo_insert_post', $postId );
}
/**
* Checks if the title/description is the same as their default format in Search Appearance and nulls it if this is the case.
* Doing this ensures that updates to the default title/description format also propogate to the post.
*
* @since 4.1.5
*
* @param int $postId The post ID.
* @param Post $thePost The Post object.
* @param array $data The data.
* @return array The data.
*/
private static function checkForDefaultFormat( $postId, $thePost, $data ) {
$data['title'] = trim( (string) $data['title'] );
$data['description'] = trim( (string) $data['description'] );
$post = aioseo()->helpers->getPost( $postId );
$defaultTitleFormat = trim( aioseo()->meta->title->getPostTypeTitle( $post->post_type ) );
$defaultDescriptionFormat = trim( aioseo()->meta->description->getPostTypeDescription( $post->post_type ) );
if ( ! empty( $data['title'] ) && $data['title'] === $defaultTitleFormat ) {
$data['title'] = null;
}
if ( ! empty( $data['description'] ) && $data['description'] === $defaultDescriptionFormat ) {
$data['description'] = null;
}
return $data;
}
/**
* Sanitize the keyphrases posted data.
*
* @since 4.2.8
*
* @param array $data An array containing the keyphrases field data.
* @return array The sanitized data.
*/
private static function sanitizeKeyphrases( $data ) {
if (
! empty( $data['focus']['analysis'] ) &&
is_array( $data['focus']['analysis'] )
) {
foreach ( $data['focus']['analysis'] as &$analysis ) {
// Remove unnecessary 'title' and 'description'.
unset( $analysis['title'] );
unset( $analysis['description'] );
}
}
if (
! empty( $data['additional'] ) &&
is_array( $data['additional'] )
) {
foreach ( $data['additional'] as &$additional ) {
if (
! empty( $additional['analysis'] ) &&
is_array( $additional['analysis'] )
) {
foreach ( $additional['analysis'] as &$additionalAnalysis ) {
// Remove unnecessary 'title' and 'description'.
unset( $additionalAnalysis['title'] );
unset( $additionalAnalysis['description'] );
}
}
}
}
return $data;
}
/**
* Sanitize the page_analysis posted data.
*
* @since 4.2.7
*
* @param array $data An array containing the page_analysis field data.
* @return array The sanitized data.
*/
private static function sanitizePageAnalysis( $data ) {
if (
empty( $data['analysis'] ) ||
! is_array( $data['analysis'] )
) {
return $data;
}
foreach ( $data['analysis'] as &$analysis ) {
foreach ( $analysis as $key => $result ) {
// Remove unnecessary data.
foreach ( [ 'title', 'description', 'highlightSentences' ] as $keyToRemove ) {
if ( isset( $analysis[ $key ][ $keyToRemove ] ) ) {
unset( $analysis[ $key ][ $keyToRemove ] );
}
}
}
}
return $data;
}
/**
* Sanitizes the post data and sets it (or the default value) to the Post object.
*
* @since 4.1.5
*
* @param int $postId The post ID.
* @param Post $thePost The Post object.
* @param array $data The data.
* @return Post The Post object with data set.
*/
protected static function sanitizeAndSetDefaults( $postId, $thePost, $data ) {
// General
$thePost->post_id = $postId;
$thePost->title = ! empty( $data['title'] ) ? sanitize_text_field( $data['title'] ) : null;
$thePost->description = ! empty( $data['description'] ) ? sanitize_text_field( $data['description'] ) : null;
$thePost->canonical_url = ! empty( $data['canonicalUrl'] ) ? sanitize_text_field( $data['canonicalUrl'] ) : null;
$thePost->keywords = ! empty( $data['keywords'] ) ? aioseo()->helpers->sanitize( $data['keywords'] ) : null;
$thePost->pillar_content = isset( $data['pillar_content'] ) ? rest_sanitize_boolean( $data['pillar_content'] ) : 0;
// TruSEO
$thePost->keyphrases = ! empty( $data['keyphrases'] ) ? self::sanitizeKeyphrases( $data['keyphrases'] ) : null;
$thePost->page_analysis = ! empty( $data['page_analysis'] ) ? self::sanitizePageAnalysis( $data['page_analysis'] ) : null;
$thePost->seo_score = ! empty( $data['seo_score'] ) ? sanitize_text_field( $data['seo_score'] ) : 0;
// Sitemap
$thePost->priority = isset( $data['priority'] ) ? ( 'default' === sanitize_text_field( $data['priority'] ) ? null : (float) $data['priority'] ) : null;
$thePost->frequency = ! empty( $data['frequency'] ) ? sanitize_text_field( $data['frequency'] ) : 'default';
// Robots Meta
$thePost->robots_default = isset( $data['default'] ) ? rest_sanitize_boolean( $data['default'] ) : 1;
$thePost->robots_noindex = isset( $data['noindex'] ) ? rest_sanitize_boolean( $data['noindex'] ) : 0;
$thePost->robots_nofollow = isset( $data['nofollow'] ) ? rest_sanitize_boolean( $data['nofollow'] ) : 0;
$thePost->robots_noarchive = isset( $data['noarchive'] ) ? rest_sanitize_boolean( $data['noarchive'] ) : 0;
$thePost->robots_notranslate = isset( $data['notranslate'] ) ? rest_sanitize_boolean( $data['notranslate'] ) : 0;
$thePost->robots_noimageindex = isset( $data['noimageindex'] ) ? rest_sanitize_boolean( $data['noimageindex'] ) : 0;
$thePost->robots_nosnippet = isset( $data['nosnippet'] ) ? rest_sanitize_boolean( $data['nosnippet'] ) : 0;
$thePost->robots_noodp = isset( $data['noodp'] ) ? rest_sanitize_boolean( $data['noodp'] ) : 0;
$thePost->robots_max_snippet = isset( $data['maxSnippet'] ) && is_numeric( $data['maxSnippet'] ) ? (int) sanitize_text_field( $data['maxSnippet'] ) : -1;
$thePost->robots_max_videopreview = isset( $data['maxVideoPreview'] ) && is_numeric( $data['maxVideoPreview'] ) ? (int) sanitize_text_field( $data['maxVideoPreview'] ) : -1;
$thePost->robots_max_imagepreview = ! empty( $data['maxImagePreview'] ) ? sanitize_text_field( $data['maxImagePreview'] ) : 'large';
// Open Graph Meta
$thePost->og_title = ! empty( $data['og_title'] ) ? sanitize_text_field( $data['og_title'] ) : null;
$thePost->og_description = ! empty( $data['og_description'] ) ? sanitize_text_field( $data['og_description'] ) : null;
$thePost->og_object_type = ! empty( $data['og_object_type'] ) ? sanitize_text_field( $data['og_object_type'] ) : 'default';
$thePost->og_image_type = ! empty( $data['og_image_type'] ) ? sanitize_text_field( $data['og_image_type'] ) : 'default';
$thePost->og_image_url = null; // We'll reset this below.
$thePost->og_image_width = null; // We'll reset this below.
$thePost->og_image_height = null; // We'll reset this below.
$thePost->og_image_custom_url = ! empty( $data['og_image_custom_url'] ) ? esc_url_raw( $data['og_image_custom_url'] ) : null;
$thePost->og_image_custom_fields = ! empty( $data['og_image_custom_fields'] ) ? sanitize_text_field( $data['og_image_custom_fields'] ) : null;
$thePost->og_video = ! empty( $data['og_video'] ) ? sanitize_text_field( $data['og_video'] ) : '';
$thePost->og_article_section = ! empty( $data['og_article_section'] ) ? sanitize_text_field( $data['og_article_section'] ) : null;
$thePost->og_article_tags = ! empty( $data['og_article_tags'] ) ? aioseo()->helpers->sanitize( $data['og_article_tags'] ) : null;
// Twitter Meta
$thePost->twitter_title = ! empty( $data['twitter_title'] ) ? sanitize_text_field( $data['twitter_title'] ) : null;
$thePost->twitter_description = ! empty( $data['twitter_description'] ) ? sanitize_text_field( $data['twitter_description'] ) : null;
$thePost->twitter_use_og = isset( $data['twitter_use_og'] ) ? rest_sanitize_boolean( $data['twitter_use_og'] ) : 0;
$thePost->twitter_card = ! empty( $data['twitter_card'] ) ? sanitize_text_field( $data['twitter_card'] ) : 'default';
$thePost->twitter_image_type = ! empty( $data['twitter_image_type'] ) ? sanitize_text_field( $data['twitter_image_type'] ) : 'default';
$thePost->twitter_image_url = null; // We'll reset this below.
$thePost->twitter_image_custom_url = ! empty( $data['twitter_image_custom_url'] ) ? esc_url_raw( $data['twitter_image_custom_url'] ) : null;
$thePost->twitter_image_custom_fields = ! empty( $data['twitter_image_custom_fields'] ) ? sanitize_text_field( $data['twitter_image_custom_fields'] ) : null;
// Schema
$thePost->schema = ! empty( $data['schema'] ) ? self::getDefaultSchemaOptions( $data['schema'] ) : null;
$thePost->local_seo = ! empty( $data['local_seo'] ) ? $data['local_seo'] : null;
$thePost->limit_modified_date = isset( $data['limit_modified_date'] ) ? rest_sanitize_boolean( $data['limit_modified_date'] ) : 0;
$thePost->open_ai = ! empty( $data['open_ai'] ) ? self::getDefaultOpenAiOptions( $data['open_ai'] ) : null;
$thePost->updated = gmdate( 'Y-m-d H:i:s' );
$thePost->primary_term = ! empty( $data['primary_term'] ) ? $data['primary_term'] : null;
$thePost->breadcrumb_settings = isset( $data['breadcrumb_settings']['default'] ) && false === $data['breadcrumb_settings']['default'] ? $data['breadcrumb_settings'] : null;
// Before we determine the OG/Twitter image, we need to set the meta data cache manually because the changes haven't been saved yet.
aioseo()->meta->metaData->bustPostCache( $thePost->post_id, $thePost );
// Set the OG/Twitter image data.
$thePost = self::setOgTwitterImageData( $thePost );
if ( ! $thePost->exists() ) {
$thePost->created = gmdate( 'Y-m-d H:i:s' );
}
// Update defaults from addons.
foreach ( aioseo()->addons->getLoadedAddons() as $addon ) {
if ( isset( $addon->postModel ) && method_exists( $addon->postModel, 'sanitizeAndSetDefaults' ) ) {
$thePost = $addon->postModel->sanitizeAndSetDefaults( $postId, $thePost, $data );
}
}
return $thePost;
}
/**
* Set the OG/Twitter image data on the post object.
*
* @since 4.1.6
*
* @param Post $thePost The Post object to modify.
* @return Post The modified Post object.
*/
public static function setOgTwitterImageData( $thePost ) {
// Set the OG image.
if (
in_array( $thePost->og_image_type, [
'featured',
'content',
'attach',
'custom',
'custom_image'
], true )
) {
// Disable the cache.
aioseo()->social->image->useCache = false;
// Set the image details.
$ogImage = aioseo()->social->facebook->getImage( $thePost->post_id );
$thePost->og_image_url = is_array( $ogImage ) ? $ogImage[0] : $ogImage;
$thePost->og_image_width = aioseo()->social->facebook->getImageWidth();
$thePost->og_image_height = aioseo()->social->facebook->getImageHeight();
// Reset the cache property.
aioseo()->social->image->useCache = true;
}
// Set the Twitter image.
if (
! $thePost->twitter_use_og &&
in_array( $thePost->twitter_image_type, [
'featured',
'content',
'attach',
'custom',
'custom_image'
], true )
) {
// Disable the cache.
aioseo()->social->image->useCache = false;
// Set the image details.
$ogImage = aioseo()->social->twitter->getImage( $thePost->post_id );
$thePost->twitter_image_url = is_array( $ogImage ) ? $ogImage[0] : $ogImage;
// Reset the cache property.
aioseo()->social->image->useCache = true;
}
return $thePost;
}
/**
* Saves some of the data as post meta so that it can be used for localization.
*
* @since 4.1.5
*
* @param int $postId The post ID.
* @param array $data The data.
* @return void
*/
public static function updatePostMeta( $postId, $data ) {
// Update the post meta as well for localization.
$keywords = ! empty( $data['keywords'] ) ? aioseo()->helpers->jsonTagsToCommaSeparatedList( $data['keywords'] ) : [];
$ogArticleTags = ! empty( $data['og_article_tags'] ) ? aioseo()->helpers->jsonTagsToCommaSeparatedList( $data['og_article_tags'] ) : [];
update_post_meta( $postId, '_aioseo_title', $data['title'] );
update_post_meta( $postId, '_aioseo_description', $data['description'] );
update_post_meta( $postId, '_aioseo_keywords', $keywords );
update_post_meta( $postId, '_aioseo_og_title', $data['og_title'] );
update_post_meta( $postId, '_aioseo_og_description', $data['og_description'] );
update_post_meta( $postId, '_aioseo_og_article_section', $data['og_article_section'] );
update_post_meta( $postId, '_aioseo_og_article_tags', $ogArticleTags );
update_post_meta( $postId, '_aioseo_twitter_title', $data['twitter_title'] );
update_post_meta( $postId, '_aioseo_twitter_description', $data['twitter_description'] );
}
/**
* Returns the default values for the TruSEO page analysis.
*
* @since 4.0.0
*
* @param object|null $pageAnalysis The page analysis object.
* @return object The default values.
*/
public static function getPageAnalysisDefaults( $pageAnalysis = null ) {
$defaults = [
'analysis' => [
'basic' => [
'lengthContent' => [
'error' => 1,
'maxScore' => 9,
'score' => 6,
],
],
'title' => [
'titleLength' => [
'error' => 1,
'maxScore' => 9,
'score' => 1,
],
],
'readability' => [
'contentHasAssets' => [
'error' => 1,
'maxScore' => 5,
'score' => 0,
],
]
]
];
if ( empty( $pageAnalysis ) ) {
return json_decode( wp_json_encode( $defaults ) );
}
return $pageAnalysis;
}
/**
* Returns a JSON object with default schema options.
*
* @since 4.2.5
*
* @param string $existingOptions The existing options in JSON.
* @param null|\WP_Post $post The post object.
* @return object The existing options with defaults added in JSON.
*/
public static function getDefaultSchemaOptions( $existingOptions = '', $post = null ) {
$defaultGraphName = aioseo()->schema->getDefaultPostTypeGraph( $post );
$defaults = [
'blockGraphs' => [],
'customGraphs' => [],
'default' => [
'data' => [
'Article' => [],
'Course' => [],
'Dataset' => [],
'FAQPage' => [],
'Movie' => [],
'Person' => [],
'Product' => [],
'ProductReview' => [],
'Car' => [],
'Recipe' => [],
'Service' => [],
'SoftwareApplication' => [],
'WebPage' => []
],
'graphName' => $defaultGraphName,
'isEnabled' => true,
],
'graphs' => []
];
if ( empty( $existingOptions ) ) {
return json_decode( wp_json_encode( $defaults ) );
}
$existingOptions = json_decode( wp_json_encode( $existingOptions ), true );
$existingOptions = array_replace_recursive( $defaults, $existingOptions );
if ( isset( $existingOptions['defaultGraph'] ) && ! empty( $existingOptions['defaultPostTypeGraph'] ) ) {
$existingOptions['default']['isEnabled'] = ! empty( $existingOptions['defaultGraph'] );
unset( $existingOptions['defaultGraph'] );
unset( $existingOptions['defaultPostTypeGraph'] );
}
// Reset the default graph type to make sure it's accurate.
if ( $defaultGraphName ) {
$existingOptions['default']['graphName'] = $defaultGraphName;
}
return json_decode( wp_json_encode( $existingOptions ) );
}
/**
* Returns the defaults for the keyphrases column.
*
* @since 4.1.7
*
* @param null|object $keyphrases The database keyphrases.
* @return object The defaults.
*/
public static function getKeyphrasesDefaults( $keyphrases = null ) {
$defaults = [
'focus' => [
'keyphrase' => '',
'score' => 0,
'analysis' => [
'keyphraseInTitle' => [
'score' => 0,
'maxScore' => 9,
'error' => 1
]
]
],
'additional' => []
];
if ( empty( $keyphrases ) ) {
return json_decode( wp_json_encode( $defaults ) );
}
if ( empty( $keyphrases->focus ) ) {
$keyphrases->focus = $defaults['focus'];
}
if ( empty( $keyphrases->additional ) ) {
$keyphrases->additional = $defaults['additional'];
}
return $keyphrases;
}
/**
* Returns the defaults for the options column.
*
* @since 4.2.2
* @version 4.2.9
*
* @param Post $post The Post object.
* @return Post The modified Post object.
*/
public static function setOptionsDefaults( $post ) {
$defaults = [
'linkFormat' => [
'internalLinkCount' => 0,
'linkAssistantDismissed' => false
],
'primaryTerm' => [
'productEducationDismissed' => false
]
];
if ( empty( $post->options ) ) {
$post->options = json_decode( wp_json_encode( $defaults ) );
return $post;
}
$post->options = json_decode( wp_json_encode( $post->options ), true );
$post->options = array_replace_recursive( $defaults, $post->options );
$post->options = json_decode( wp_json_encode( $post->options ) );
return $post;
}
/**
* Returns the default Open AI options.
*
* @since 4.3.2
*
* @param array $existingOptions The existing options.
* @return object The default options.
*/
public static function getDefaultOpenAiOptions( $existingOptions = [] ) {
$defaults = [
'title' => [
'suggestions' => [],
'usage' => 0
],
'description' => [
'suggestions' => [],
'usage' => 0
]
];
if ( empty( $existingOptions ) ) {
return json_decode( wp_json_encode( $defaults ) );
}
$existingOptions = json_decode( wp_json_encode( $existingOptions ), true );
$existingOptions = array_replace_recursive( $defaults, $existingOptions );
return json_decode( wp_json_encode( $existingOptions ) );
}
/**
* Returns the default breadcrumb settings options.
*
* @since 4.8.3
*
* @param array $postType The post type.
* @param array $existingOptions The existing options.
* @return object The default options.
*/
public static function getDefaultBreadcrumbSettingsOptions( $postType, $existingOptions = [] ) {
$default = aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->useDefaultTemplate ?? true;
$showHomeCrumb = $default ? aioseo()->options->breadcrumbs->homepageLink : aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->showHomeCrumb ?? true;
$defaults = [
'default' => true,
'separator' => aioseo()->options->breadcrumbs->separator,
'showHomeCrumb' => $showHomeCrumb ?? true,
'showTaxonomyCrumbs' => aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->showTaxonomyCrumbs ?? true,
'showParentCrumbs' => aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->showParentCrumbs ?? true,
'template' => aioseo()->helpers->encodeOutputHtml( aioseo()->breadcrumbs->frontend->getDefaultTemplate( 'single' ) ),
'parentTemplate' => aioseo()->helpers->encodeOutputHtml( aioseo()->breadcrumbs->frontend->getDefaultTemplate( 'single' ) ),
'taxonomy' => aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->taxonomy ?? '',
'primaryTerm' => null
];
if ( empty( $existingOptions ) ) {
return json_decode( wp_json_encode( $defaults ) );
}
$existingOptions = json_decode( wp_json_encode( $existingOptions ), true );
$existingOptions = array_replace_recursive( $defaults, $existingOptions );
return json_decode( wp_json_encode( $existingOptions ) );
}
/**
* Migrates the post's audience age schema data when it is loaded.
* Min age: [0 => newborns, 0.25 => infants, 1 => toddlers, 5 => kids, 13 => adults]
* Max age: [0.25 => newborns, 1 => infants, 5 => toddlers, 13 => kids]
*
* @since 4.7.9
*
* @param object $audience The audience data.
* @return object
*/
public static function migratePostAudienceAgeSchema( $audience ) {
$ages = [ 0, 0.25, 1, 5, 13 ];
// converts variable to integer if it's a number otherwise is null.
$parsedMinAge = filter_var( $audience->minimumAge, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE );
$parsedMaxAge = filter_var( $audience->maximumAge, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE );
if ( null === $parsedMinAge && null === $parsedMaxAge ) {
return $audience;
}
$minAge = is_numeric( $parsedMinAge ) ? $parsedMinAge : 0;
$maxAge = is_numeric( $parsedMaxAge ) ? $parsedMaxAge : null;
// get the minimumAge if available or the nearest bigger one.
foreach ( $ages as $age ) {
if ( $age >= $minAge ) {
$audience->minimumAge = $age;
break;
}
}
// get the maximumAge if available or the nearest bigger one.
foreach ( $ages as $age ) {
if ( $age >= $maxAge ) {
$maxAge = $age;
break;
}
}
// makes sure the maximumAge is 13 below
if ( null !== $maxAge ) {
$audience->maximumAge = 13 < $maxAge ? 13 : $maxAge;
}
// Minimum age 13 is for adults.
// If minimumAge is still higher or equal 13 then it's for adults and maximumAge should be empty.
if ( 13 <= $audience->minimumAge ) {
$audience->minimumAge = 13;
$audience->maximumAge = null;
}
return $audience;
}
/**
* Migrates update Korea country code for Person, Product, Event, and JobsPosting schemas.
*
* @since 4.7.1
*
* @param Post $aioseoPost The post object.
* @return Post The modified post object.
*/
private static function migrateKoreaCountryCodeSchemas( $aioseoPost ) {
if ( empty( $aioseoPost->schema ) || empty( $aioseoPost->schema->graphs ) ) {
return $aioseoPost;
}
foreach ( $aioseoPost->schema->graphs as $key => $graph ) {
if ( isset( $aioseoPost->schema->graphs[ $key ]->properties->location->country ) ) {
$aioseoPost->schema->graphs[ $key ]->properties->location->country = self::invertKoreaCode( $graph->properties->location->country );
}
if ( isset( $aioseoPost->schema->graphs[ $key ]->properties->shippingDestinations ) ) {
$aioseoPost->schema->graphs[ $key ]->properties->shippingDestinations = array_map( function( $item ) {
$item->country = self::invertKoreaCode( $item->country );
return $item;
}, $graph->properties->shippingDestinations );
}
}
$aioseoPost->save();
return $aioseoPost;
}
/**
* Utility function to invert the country code for Korea.
*
* @since 4.7.1
*
* @param string $code country code.
* @return string country code.
*/
public static function invertKoreaCode( $code ) {
return 'KP' === $code ? 'KR' : $code;
}
} Models/WritingAssistantKeyword.php 0000666 00000003047 15113050716 0013371 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Models;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Keyword.
*
* @since 4.7.4
*/
class WritingAssistantKeyword extends Model {
/**
* The name of the table in the database, without the prefix.
*
* @since 4.7.4
*
* @var string
*/
protected $table = 'aioseo_writing_assistant_keywords';
/**
* Fields that should be numeric values.
*
* @since 4.7.4
*
* @var array
*/
protected $integerFields = [ 'id', 'progress' ];
/**
* Fields that should be boolean values.
*
* @since 4.7.4
*
* @var array
*/
protected $booleanFields = [];
/**
* Fields that should be encoded/decoded on save/get.
*
* @since 4.7.4
*
* @var array
*/
protected $jsonFields = [ 'keywords', 'competitors', 'content_analysis' ];
/**
* Gets a keyword.
*
* @since 4.7.4
*
* @param string $keyword A keyword.
* @param string $country The country code.
* @param string $language The language code.
* @return object A keyword found.
*/
public static function getKeyword( $keyword, $country, $language ) {
$dbKeyword = aioseo()->core->db->start( 'aioseo_writing_assistant_keywords' )
->where( 'keyword', $keyword )
->where( 'country', $country )
->where( 'language', $language )
->run()
->model( 'AIOSEO\Plugin\Common\Models\WritingAssistantKeyword' );
if ( ! $dbKeyword->exists() ) {
$dbKeyword->keyword = $keyword;
$dbKeyword->country = $country;
$dbKeyword->language = $language;
}
return $dbKeyword;
}
} Models/Notification.php 0000666 00000023010 15113050716 0011125 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Models;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* The Notification DB Model.
*
* @since 4.0.0
*/
class Notification extends Model {
/**
* The name of the table in the database, without the prefix.
*
* @since 4.0.0
*
* @var string
*/
protected $table = 'aioseo_notifications';
/**
* An array of fields to set to null if already empty when saving to the database.
*
* @since 4.0.0
*
* @var array
*/
protected $nullFields = [
'start',
'end',
'notification_id',
'notification_name',
'button1_label',
'button1_action',
'button2_label',
'button2_action'
];
/**
* Fields that should be json encoded on save and decoded on get.
*
* @since 4.0.0
*
* @var array
*/
protected $jsonFields = [ 'level' ];
/**
* Fields that should be boolean values.
*
* @since 4.0.0
*
* @var array
*/
protected $booleanFields = [ 'dismissed' ];
/**
* Fields that should be hidden when serialized.
*
* @var array
*/
protected $hidden = [ 'id' ];
/**
* An array of fields attached to this resource.
*
* @since 4.0.0
*
* @var array
*/
protected $columns = [
'id',
'slug',
'addon',
'title',
'content',
'type',
'level',
'notification_id',
'notification_name',
'start',
'end',
'button1_label',
'button1_action',
'button2_label',
'button2_action',
'dismissed',
'new',
'created',
'updated'
];
/**
* Get the list of notifications.
*
* @since 4.1.3
*
* @param bool $reset Whether or not to reset the notifications.
* @return array An array of notifications.
*/
public static function getNotifications( $reset = true ) {
static $notifications = null;
if ( null !== $notifications ) {
return $notifications;
}
$notifications = [
'active' => self::getAllActiveNotifications(),
'new' => self::getNewNotifications( $reset ),
'dismissed' => self::getAllDismissedNotifications()
];
return $notifications;
}
/**
* Get an array of active notifications.
*
* @since 4.0.0
*
* @return array An array of active notifications.
*/
public static function getAllActiveNotifications() {
static $activeNotifications = null;
if ( null !== $activeNotifications ) {
return $activeNotifications;
}
$staticNotifications = self::getStaticNotifications();
$notifications = array_values( json_decode( wp_json_encode( self::getActiveNotifications() ), true ) );
$activeNotifications = ! empty( $staticNotifications )
? array_merge( $staticNotifications, $notifications )
: $notifications;
return $activeNotifications;
}
/**
* Get all new notifications. After retrieving them, this will reset them.
* This means that calling this method twice will result in no results
* the second time. The only exception is to pass false as a reset variable to prevent it.
*
* @since 4.1.3
*
* @param bool $reset Whether or not to reset the new notifications.
* @return array An array of new notifications if any exist.
*/
public static function getNewNotifications( $reset = true ) {
static $newNotifications = null;
if ( null !== $newNotifications ) {
return $newNotifications;
}
$newNotifications = self::filterNotifications(
aioseo()->core->db
->start( 'aioseo_notifications' )
->where( 'dismissed', 0 )
->where( 'new', 1 )
->whereRaw( "(start <= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR start IS NULL)" )
->whereRaw( "(end >= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR end IS NULL)" )
->orderBy( 'start DESC' )
->orderBy( 'created DESC' )
->run()
->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' )
);
if ( $reset ) {
self::resetNewNotifications();
}
return $newNotifications;
}
/**
* Resets all new notifications.
*
* @since 4.1.3
*
* @return void
*/
public static function resetNewNotifications() {
aioseo()->core->db
->update( 'aioseo_notifications' )
->where( 'new', 1 )
->set( 'new', 0 )
->run();
}
/**
* Returns all static notifications.
*
* @since 4.1.2
*
* @return array An array of static notifications.
*/
public static function getStaticNotifications() {
$staticNotifications = [];
$notifications = [
'unlicensed-addons',
'review'
];
foreach ( $notifications as $notification ) {
switch ( $notification ) {
case 'review':
// If they intentionally dismissed the main notification, we don't show the repeat one.
$originalDismissed = get_user_meta( get_current_user_id(), '_aioseo_plugin_review_dismissed', true );
if ( '4' !== $originalDismissed ) {
break;
}
$dismissed = get_user_meta( get_current_user_id(), '_aioseo_notification_plugin_review_dismissed', true );
if ( '3' === $dismissed ) {
break;
}
if ( ! empty( $dismissed ) && $dismissed > time() ) {
break;
}
$activated = aioseo()->internalOptions->internal->firstActivated( time() );
if ( $activated > strtotime( '-20 days' ) ) {
break;
}
$isV3 = get_option( 'aioseop_options' ) || get_option( 'aioseo_options_v3' );
$staticNotifications[] = [
'slug' => 'notification-' . $notification,
'component' => 'notifications-' . $notification . ( $isV3 ? '' : '2' )
];
break;
case 'unlicensed-addons':
$unlicensedAddons = aioseo()->addons->unlicensedAddons();
if ( empty( $unlicensedAddons['addons'] ) ) {
break;
}
$staticNotifications[] = [
'slug' => 'notification-' . $notification,
'component' => 'notifications-' . $notification,
'addons' => $unlicensedAddons['addons'],
'message' => $unlicensedAddons['message']
];
break;
}
}
return $staticNotifications;
}
/**
* Retrieve active notifications.
*
* @since 4.0.0
*
* @return array An array of active notifications or empty.
*/
protected static function getActiveNotifications() {
return self::filterNotifications(
aioseo()->core->db
->start( 'aioseo_notifications' )
->where( 'dismissed', 0 )
->whereRaw( "(start <= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR start IS NULL)" )
->whereRaw( "(end >= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR end IS NULL)" )
->orderBy( 'start DESC' )
->orderBy( 'created DESC' )
->run()
->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' )
);
}
/**
* Get an array of dismissed notifications.
*
* @since 4.0.0
*
* @return array An array of dismissed notifications.
*/
protected static function getAllDismissedNotifications() {
return array_values( json_decode( wp_json_encode( self::getDismissedNotifications() ), true ) );
}
/**
* Retrieve dismissed notifications.
*
* @since 4.0.0
*
* @return array An array of dismissed notifications or empty.
*/
protected static function getDismissedNotifications() {
static $dismissedNotifications = null;
if ( null !== $dismissedNotifications ) {
return $dismissedNotifications;
}
$dismissedNotifications = self::filterNotifications(
aioseo()->core->db
->start( 'aioseo_notifications' )
->where( 'dismissed', 1 )
->orderBy( 'updated DESC' )
->run()
->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' )
);
return $dismissedNotifications;
}
/**
* Returns a notification by its name.
*
* @since 4.0.0
*
* @param string $name The notification name.
* @return Notification The notification.
*/
public static function getNotificationByName( $name ) {
return aioseo()->core->db
->start( 'aioseo_notifications' )
->where( 'notification_name', $name )
->run()
->model( 'AIOSEO\\Plugin\\Common\\Models\\Notification' );
}
/**
* Stores a new notification in the DB.
*
* @since 4.0.0
*
* @param array $fields The fields.
* @return Notification $notification The notification.
*/
public static function addNotification( $fields ) {
// Set the dismissed status to false.
$fields['dismissed'] = 0;
$notification = new self();
$notification->set( $fields );
$notification->save();
return $notification;
}
/**
* Deletes a notification by its name.
*
* @since 4.0.0
*
* @param string $name The notification name.
* @return void
*/
public static function deleteNotificationByName( $name ) {
aioseo()->core->db
->delete( 'aioseo_notifications' )
->where( 'notification_name', $name )
->run();
}
/**
* Filters the notifications based on the targeted plan levels.
*
* @since 4.0.0
*
* @param array $notifications The notifications
* @return array $remainingNotifications The remaining notifications.
*/
protected static function filterNotifications( $notifications ) {
$remainingNotifications = [];
foreach ( $notifications as $notification ) {
// If announcements are disabled and this is an announcement, skip adding it and move on.
if (
! aioseo()->options->advanced->announcements &&
'success' === $notification->type
) {
continue;
}
// If this is an addon notification and the addon is disabled, skip adding it and move on.
if ( ! empty( $notification->addon ) && ! aioseo()->addons->getLoadedAddon( $notification->addon ) ) {
continue;
}
$levels = $notification->level;
if ( ! is_array( $levels ) ) {
$levels = empty( $notification->level ) ? [ 'all' ] : [ $notification->level ];
}
foreach ( $levels as $level ) {
if ( ! aioseo()->notices->validateType( $level ) ) {
continue 2;
}
}
$remainingNotifications[] = $notification;
}
return $remainingNotifications;
}
} Models/SeoAnalyzerResult.php 0000666 00000011075 15113050716 0012142 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Models;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* The SeoAnalyzerResult Model.
*
* @since 4.8.3
*/
class SeoAnalyzerResult extends Model {
/**
* The name of the table in the database, without the prefix.
*
* @since 4.8.3
*
* @var string
*/
protected $table = 'aioseo_seo_analyzer_results';
/**
* Fields that should be json encoded on save and decoded on get.
*
* @since 4.8.3
*
* @var array
*/
protected $jsonFields = [
'data'
];
/**
* Fields that should be hidden when serialized.
*
* @since 4.8.3
*
* @var array
*/
protected $hidden = [ 'id' ];
/**
* Fields that can be null when saved.
*
* @since 4.8.3
*
* @var array
*/
protected $nullFields = [
'competitor_url',
];
/**
* An array of columns from the DB that we can use.
*
* @since 4.8.3
*
* @var array
*/
protected $columns = [
'id',
'score',
'data',
'competitor_url',
'created',
'updated',
];
/**
* Returns all not competitors results.
*
* @since 4.8.3
*
* @return array List of results.
*/
public static function getResults() {
$results = aioseo()->core->db->start( 'aioseo_seo_analyzer_results' )
->select( '*' )
->where( 'competitor_url', null )
->run()
->result();
if ( empty( $results ) ) {
return [];
}
return self::parseObjects( $results );
}
/**
* Returns all competitors results.
*
* @since 4.8.3
*
* @return array List of results.
*/
public static function getCompetitorsResults() {
$results = aioseo()->core->db->start( 'aioseo_seo_analyzer_results' )
->select( '*' )
->whereRaw( 'competitor_url IS NOT NULL' )
->orderBy( 'updated DESC' )
->run()
->result();
if ( empty( $results ) ) {
return [];
}
return self::parseObjects( $results, true );
}
/**
* Parse results to the front end format.
*
* @since 4.8.3
*
* @param array $objects List of objects.
* @param bool $isCompetitor Flag that indicates if is parsing a competitor or a homepage result.
* @return array List of results.
*/
private static function parseObjects( $objects, $isCompetitor = false ) {
$results = [];
foreach ( $objects as $obj ) {
$data = json_decode( $obj->data ?? '[]', true );
if ( ! $isCompetitor ) {
$results['score'] = $obj->score ?? 0;
}
foreach ( $data as $result ) {
$metadata = $result['metadata'] ?? [];
$item = empty( $result['status'] ) && ! empty( $metadata['value'] ) ? $metadata['value'] : array_merge( $metadata, [ 'status' => $result['status'] ] );
if ( $isCompetitor ) {
if ( empty( $obj->competitor_url ) || empty( $result['group'] ) || empty( $result['name'] ) ) {
continue;
}
$results[ $obj->competitor_url ]['results'][ $result['group'] ][ $result['name'] ] = $item;
$results[ $obj->competitor_url ]['score'] = ! empty( $obj->score ) ? $obj->score : 0;
} else {
$results['results'][ $result['group'] ][ $result['name'] ] = $item;
}
}
}
return $results;
}
/**
* Delete results by competitor url, if null we are deleting the homepage results.
*
* @since 4.8.3
*
* @param string $url The competitor url.
* @return void
*/
public static function deleteByUrl( $url ) {
aioseo()->core->db
->delete( 'aioseo_seo_analyzer_results' )
->where( 'competitor_url', $url )
->run();
}
/**
* Add multiple results at once.
*
* @since 4.8.3
*
* @return void
*/
public static function addResults( $results, $competitorUrl = null ) {
if ( empty( $results['results'] ) ) {
return;
}
// Delete the results for the competitor url if it exists.
self::deleteByUrl( $competitorUrl );
$data = [
'competitor_url' => $competitorUrl,
'score' => $results['score'],
'data' => []
];
foreach ( $results['results'] as $group => $items ) {
foreach ( $items as $name => $result ) {
$fields = [
'name' => $name,
'group' => $group,
'status' => empty( $result['status'] ) ? null : $result['status'],
'metadata' => null,
];
if ( ! is_array( $result ) ) {
$fields['metadata'] = [ 'value' => $result ];
} else {
$metadata = [];
foreach ( $result as $key => $value ) {
if ( 'status' !== $key ) {
$metadata[ $key ] = $value;
}
}
if ( ! empty( $metadata ) ) {
$fields['metadata'] = $metadata;
}
}
$data['data'][] = $fields;
}
}
$data['data'] = wp_json_encode( $data['data'] );
$newResult = new SeoAnalyzerResult( $data );
$newResult->save();
}
} Models/CrawlCleanupLog.php 0000666 00000003434 15113050716 0011531 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Models;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models as CommonModels;
/**
* The Crawl Cleanup Log DB Model.
*
* @since 4.5.8
*/
class CrawlCleanupLog extends CommonModels\Model {
/**
* The name of the table in the database, without the prefix.
*
* @since 4.5.8
*
* @var string
*/
protected $table = 'aioseo_crawl_cleanup_logs';
/**
* Fields that should be hidden when serialized.
*
* @since 4.5.8
*
* @var array
*/
protected $hidden = [ 'id' ];
/**
* Fields that should be numeric values.
*
* @since 4.5.8
*
* @var array
*/
protected $integerFields = [ 'id', 'hits' ];
/**
* Field to count hits.
*
* @since 4.5.8
*
* @var integer
*/
public $hits = 0;
/**
* Create a Log in case it doesn't exist.
*
* @since 4.5.8
*
* @return void
*/
public function create() {
if ( null !== $this->id ) {
$this->hits++;
}
parent::save();
}
/**
* Get Crawl Cleanup passing Slug
*
* @since 4.5.8
*
* @param string $slug The Slug to search.
* @return CrawlCleanupLog The CrawlCleanupLog object.
*/
public static function getBySlug( $slug ) {
return aioseo()->core->db
->start( 'aioseo_crawl_cleanup_logs' )
->where( 'hash', sha1( $slug ) )
->run()
->model( 'AIOSEO\\Plugin\\Common\\Models\\CrawlCleanupLog' );
}
/**
* Transforms data as needed.
*
* @since 4.5.8
*
* @param array $data The data array to transform.
* @return array The transformed data.
*/
protected function transform( $data, $set = false ) {
$data = parent::transform( $data, $set );
// Create slug hash.
if ( ! empty( $data['slug'] ) ) {
$data['hash'] = sha1( $data['slug'] );
}
return $data;
}
} Meta/MetaData.php 0000666 00000007556 15113050716 0007643 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Meta;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Handles fetching metadata for the current object.
*
* @since 4.0.0
*/
class MetaData {
/**
* The cached meta data for posts.
*
* @since 4.1.7
*
* @var array
*/
private $posts = [];
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
add_action( 'wpml_pro_translation_completed', [ $this, 'updateWpmlLocalization' ], 1000, 3 );
}
/**
* Update the localized data in our posts table.
*
* @since 4.0.0
*
* @param integer $postId The post ID.
* @param array $fields An array of fields to update.
* @return void
*/
public function updateWpmlLocalization( $postId, $fields = [], $job = null ) {
$aioseoFields = [
'_aioseo_title',
'_aioseo_description',
'_aioseo_keywords',
'_aioseo_og_title',
'_aioseo_og_description',
'_aioseo_twitter_title',
'_aioseo_twitter_description'
];
$parentId = $job->original_doc_id;
$parentPost = Models\Post::getPost( $parentId );
$currentPost = Models\Post::getPost( $postId );
$columns = $parentPost->getColumns();
foreach ( $columns as $column => $value ) {
// Skip the ID columns.
if ( 'id' === $column || 'post_id' === $column ) {
continue;
}
$currentPost->$column = $parentPost->$column;
}
$currentPost->post_id = $postId;
foreach ( $aioseoFields as $aioseoField ) {
if ( ! empty( $fields[ 'field-' . $aioseoField . '-0' ] ) ) {
$value = $fields[ 'field-' . $aioseoField . '-0' ]['data'];
if ( '_aioseo_keywords' === $aioseoField ) {
$value = explode( ',', $value );
foreach ( $value as $k => $keyword ) {
$value[ $k ] = [
'label' => $keyword,
'value' => $keyword
];
}
$value = wp_json_encode( $value );
}
$currentPost->{ str_replace( '_aioseo_', '', $aioseoField ) } = $value;
}
}
$currentPost->save();
}
/**
* Returns the metadata for the current object.
*
* @since 4.0.0
*
* @param \WP_Post $post The post object (optional).
* @return Models\Post|bool The meta data or false.
*/
public function getMetaData( $post = null ) {
if ( ! $post ) {
$post = aioseo()->helpers->getPost();
}
if ( $post ) {
$post = is_object( $post ) ? $post : aioseo()->helpers->getPost( $post );
// If we still have no post, let's return false.
if ( ! is_a( $post, 'WP_Post' ) ) {
return false;
}
if ( isset( $this->posts[ $post->ID ] ) ) {
return $this->posts[ $post->ID ];
}
$this->posts[ $post->ID ] = Models\Post::getPost( $post->ID );
if ( ! $this->posts[ $post->ID ]->exists() ) {
$migratedMeta = aioseo()->migration->meta->getMigratedPostMeta( $post->ID );
if ( ! empty( $migratedMeta ) ) {
foreach ( $migratedMeta as $k => $v ) {
$this->posts[ $post->ID ]->{$k} = $v;
}
$this->posts[ $post->ID ]->save();
}
}
return $this->posts[ $post->ID ];
}
return false;
}
/**
* Returns the cached OG image from the meta data.
*
* @since 4.1.6
*
* @param Object $metaData The meta data object.
* @return array An array of image data.
*/
public function getCachedOgImage( $metaData ) {
return [
$metaData->og_image_url,
isset( $metaData->og_image_width ) ? $metaData->og_image_width : null,
isset( $metaData->og_image_height ) ? $metaData->og_image_height : null
];
}
/**
* Busts the meta data cache for a given post.
*
* @since 4.1.7
*
* @param int $postId The post ID.
* @param Models\Post $metaData The meta data.
* @return void
*/
public function bustPostCache( $postId, $metaData = null ) {
if ( null === $metaData || ! is_a( $metaData, 'AIOSEO\Plugin\Common\Models\Post' ) ) {
unset( $this->posts[ $postId ] );
}
$this->posts[ $postId ] = $metaData;
}
} Meta/Description.php 0000666 00000021263 15113050716 0010435 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Meta;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
/**
* Handles the (Open Graph) description.
*
* @since 4.0.0
*/
class Description {
/**
* Helpers class instance.
*
* @since 4.2.7
*
* @var Helpers
*/
public $helpers = null;
/**
* Class constructor.
*
* @since 4.1.2
*/
public function __construct() {
$this->helpers = new Helpers( 'description' );
}
/**
* Returns the homepage description.
*
* @since 4.0.0
*
* @return string The homepage description.
*/
public function getHomePageDescription() {
if ( 'page' === get_option( 'show_on_front' ) ) {
$description = $this->getPostDescription( (int) get_option( 'page_on_front' ) );
return $description ? $description : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) );
}
$description = aioseo()->options->searchAppearance->global->metaDescription;
if ( aioseo()->helpers->isWpmlActive() ) {
// Allow WPML to translate the title if the homepage is not static.
$description = apply_filters( 'wpml_translate_single_string', $description, 'admin_texts_aioseo_options_localized', '[aioseo_options_localized]searchAppearance_global_metaDescription' );
}
$description = $this->helpers->prepare( $description );
return $description ? $description : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) );
}
/**
* Returns the description for the current page.
*
* @since 4.0.0
*
* @param \WP_Post $post The post object (optional).
* @param boolean $default Whether we want the default value, not the post one.
* @return string The page description.
*/
public function getDescription( $post = null, $default = false ) {
if ( BuddyPressIntegration::isComponentPage() ) {
return aioseo()->standalone->buddyPress->component->getMeta( 'description' );
}
if ( is_home() ) {
return $this->getHomePageDescription();
}
if ( $post || is_singular() || aioseo()->helpers->isStaticPage() ) {
$description = $this->getPostDescription( $post, $default );
if ( $description ) {
return $description;
}
if ( is_attachment() ) {
$post = empty( $post ) ? aioseo()->helpers->getPost() : $post;
$caption = wp_get_attachment_caption( $post->ID );
return $caption ? $this->helpers->prepare( $caption ) : $this->helpers->prepare( $post->post_content );
}
}
if ( is_category() || is_tag() || is_tax() ) {
$term = $post ? $post : aioseo()->helpers->getTerm();
return $this->getTermDescription( $term, $default );
}
if ( is_author() ) {
$description = $this->helpers->prepare( aioseo()->options->searchAppearance->archives->author->metaDescription );
if ( $description ) {
return $description;
}
$author = get_queried_object();
return $author ? $this->helpers->prepare( get_the_author_meta( 'description', $author->ID ) ) : '';
}
if ( is_date() ) {
return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->date->metaDescription );
}
if ( is_search() ) {
return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->search->metaDescription );
}
if ( is_post_type_archive() ) {
$postType = get_queried_object();
if ( is_a( $postType, 'WP_Post_Type' ) ) {
return $this->helpers->prepare( $this->getArchiveDescription( $postType->name ) );
}
}
return '';
}
/**
* Returns the description for a given post.
*
* @since 4.0.0
*
* @param \WP_Post|int $post The post object or ID.
* @param boolean $default Whether we want the default value, not the post one.
* @return string The post description.
*/
public function getPostDescription( $post, $default = false ) {
$post = $post && is_object( $post ) ? $post : aioseo()->helpers->getPost( $post );
if ( ! is_a( $post, 'WP_Post' ) ) {
return '';
}
static $posts = [];
if ( isset( $posts[ $post->ID ] ) ) {
return $posts[ $post->ID ];
}
$description = '';
$metaData = aioseo()->meta->metaData->getMetaData( $post );
if ( ! empty( $metaData->description ) && ! $default ) {
$description = $this->helpers->prepare( $metaData->description, $post->ID, false );
}
if (
$description ||
(
in_array( 'autogenerateDescriptions', aioseo()->internalOptions->deprecatedOptions, true ) &&
! aioseo()->options->deprecated->searchAppearance->advanced->autogenerateDescriptions
)
) {
$posts[ $post->ID ] = $description;
return $description;
}
$description = $this->helpers->sanitize( $this->getPostTypeDescription( $post->post_type ), $post->ID, $default );
$generateDescriptions = apply_filters( 'aioseo_generate_descriptions_from_content', true, [ $post ] );
if ( ! $description && ! post_password_required( $post ) ) {
$description = $post->post_excerpt;
if (
$generateDescriptions &&
in_array( 'useContentForAutogeneratedDescriptions', aioseo()->internalOptions->deprecatedOptions, true ) &&
aioseo()->options->deprecated->searchAppearance->advanced->useContentForAutogeneratedDescriptions
) {
$description = aioseo()->helpers->getDescriptionFromContent( $post );
}
$description = $this->helpers->sanitize( $description, $post->ID, $default );
if ( ! $description && $generateDescriptions && $post->post_content ) {
$description = $this->helpers->sanitize( aioseo()->helpers->getDescriptionFromContent( $post ), $post->ID, $default );
}
}
if ( ! is_paged() ) {
if ( in_array( 'descriptionFormat', aioseo()->internalOptions->deprecatedOptions, true ) ) {
$descriptionFormat = aioseo()->options->deprecated->searchAppearance->global->descriptionFormat;
if ( $descriptionFormat ) {
$description = preg_replace( '/#description/', $description, (string) $descriptionFormat );
}
}
}
$posts[ $post->ID ] = $description ? $this->helpers->prepare( $description, $post->ID, $default ) : $this->helpers->prepare( term_description( '' ), $post->ID, $default );
return $posts[ $post->ID ];
}
/**
* Retrieve the default description for the archive template.
*
* @since 4.7.6
*
* @param string $postType The custom post type.
* @return string The description.
*/
public function getArchiveDescription( $postType ) {
static $archiveDescription = [];
if ( isset( $archiveDescription[ $postType ] ) ) {
return $archiveDescription[ $postType ];
}
$archiveDescription[ $postType ] = '';
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
if ( $dynamicOptions->searchAppearance->archives->has( $postType ) ) {
$archiveDescription[ $postType ] = aioseo()->dynamicOptions->searchAppearance->archives->{$postType}->metaDescription;
}
return $archiveDescription[ $postType ];
}
/**
* Retrieve the default description for the post type.
*
* @since 4.0.6
*
* @param string $postType The post type.
* @return string The description.
*/
public function getPostTypeDescription( $postType ) {
static $postTypeDescription = [];
if ( isset( $postTypeDescription[ $postType ] ) ) {
return $postTypeDescription[ $postType ];
}
if ( aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
$description = aioseo()->dynamicOptions->searchAppearance->postTypes->{$postType}->metaDescription;
}
$postTypeDescription[ $postType ] = empty( $description ) ? '' : $description;
return $postTypeDescription[ $postType ];
}
/**
* Returns the term description.
*
* @since 4.0.6
*
* @param \WP_Term $term The term object.
* @param boolean $default Whether we want the default value, not the post one.
* @return string The term description.
*/
public function getTermDescription( $term, $default = false ) {
if ( ! is_a( $term, 'WP_Term' ) ) {
return '';
}
static $terms = [];
if ( isset( $terms[ $term->term_id ] ) ) {
return $terms[ $term->term_id ];
}
$description = '';
if (
in_array( 'autogenerateDescriptions', aioseo()->internalOptions->deprecatedOptions, true ) &&
! aioseo()->options->deprecated->searchAppearance->advanced->autogenerateDescriptions
) {
$terms[ $term->term_id ] = $description;
return $description;
}
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
if ( ! $description && $dynamicOptions->searchAppearance->taxonomies->has( $term->taxonomy ) ) {
$description = $this->helpers->prepare( aioseo()->dynamicOptions->searchAppearance->taxonomies->{$term->taxonomy}->metaDescription, false, $default );
}
$terms[ $term->term_id ] = $description ? $description : $this->helpers->prepare( term_description( $term->term_id ), false, $default );
return $terms[ $term->term_id ];
}
} Meta/Meta.php 0000666 00000002700 15113050716 0007033 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Meta;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Instantiates the Meta classes.
*
* @since 4.0.0
*/
class Meta {
/**
* MetaData class instance.
*
* @since 4.2.7
*
* @var MetaData
*/
public $metaData = null;
/**
* Title class instance.
*
* @since 4.2.7
*
* @var Title
*/
public $title = null;
/**
* Description class instance.
*
* @since 4.2.7
*
* @var Description
*/
public $description = null;
/**
* Keywords class instance.
*
* @since 4.2.7
*
* @var Keywords
*/
public $keywords = null;
/**
* Robots class instance.
*
* @since 4.2.7
*
* @var Robots
*/
public $robots = null;
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
$this->metaData = new MetaData();
$this->title = new Title();
$this->description = new Description();
$this->keywords = new Keywords();
$this->robots = new Robots();
new Amp();
new Links();
add_action( 'delete_post', [ $this, 'deletePostMeta' ], 1000 );
}
/**
* When we delete the meta, we want to delete our post model.
*
* @since 4.0.1
*
* @param integer $postId The ID of the post.
* @return void
*/
public function deletePostMeta( $postId ) {
$aioseoPost = Models\Post::getPost( $postId );
if ( $aioseoPost->exists() ) {
$aioseoPost->delete();
}
}
} Meta/Amp.php 0000666 00000002202 15113050716 0006657 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Meta;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Adds support for Google AMP.
*
* @since 4.0.0
*/
class Amp {
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
add_action( 'init', [ $this, 'runAmp' ] );
}
/**
* Run the AMP hooks.
*
* @since 4.0.0
*
* @return void
*/
public function runAmp() {
if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
return;
}
// Add social meta to AMP plugin.
$enableAmp = apply_filters( 'aioseo_enable_amp_social_meta', true );
if ( $enableAmp ) {
$useSchema = apply_filters( 'aioseo_amp_schema', true );
if ( $useSchema ) {
add_action( 'amp_post_template_head', [ $this, 'removeHooksAmpSchema' ], 9 );
}
add_action( 'amp_post_template_head', [ aioseo()->head, 'output' ], 11 );
}
}
/**
* Remove Hooks with AMP's Schema.
*
* @since 4.0.0
*
* @return void
*/
public function removeHooksAmpSchema() {
// Remove AMP Schema hook used for outputting data.
remove_action( 'amp_post_template_head', 'amp_print_schemaorg_metadata' );
}
} Meta/Included.php 0000666 00000006250 15113050716 0007700 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Meta;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* To check whether SEO is enabled for the queried object.
*
* @since 4.0.0
*/
class Included {
/**
* Checks whether the queried object is included.
*
* @since 4.0.0
*
* @return bool
*/
public function isIncluded() {
if ( is_admin() || is_feed() ) {
return false;
}
if ( apply_filters( 'aioseo_disable', false ) || $this->isExcludedGlobal() ) {
return false;
}
if ( ! $this->isQueriedObjectPublic() ) {
return false;
}
return true;
}
/**
* Checks whether the queried object is public.
*
* @since 4.2.2
*
* @return bool Whether the queried object is public.
*/
protected function isQueriedObjectPublic() {
$queriedObject = get_queried_object(); // Don't use the getTerm helper here.
if ( is_a( $queriedObject, 'WP_Post' ) ) {
return aioseo()->helpers->isPostTypePublic( $queriedObject->post_type );
}
// Check if the current page is a post type archive page.
if ( is_a( $queriedObject, 'WP_Post_Type' ) ) {
return aioseo()->helpers->isPostTypePublic( $queriedObject->name );
}
if ( is_a( $queriedObject, 'WP_Term' ) ) {
if ( aioseo()->helpers->isWooCommerceProductAttribute( $queriedObject->taxonomy ) ) {
// Check if the attribute has archives enabled.
$taxonomy = get_taxonomy( $queriedObject->taxonomy );
return $taxonomy->public;
}
return aioseo()->helpers->isTaxonomyPublic( $queriedObject->taxonomy );
}
// Return true in all other cases (e.g. search page, date archive, etc.).
return true;
}
/**
* Checks whether the queried object has been excluded globally.
*
* @since 4.0.0
*
* @return bool
*/
protected function isExcludedGlobal() {
if ( is_category() || is_tag() || is_tax() ) {
return $this->isTaxExcludedGlobal();
}
if ( ! in_array( 'excludePosts', aioseo()->internalOptions->deprecatedOptions, true ) ) {
return false;
}
$excludedPosts = aioseo()->options->deprecated->searchAppearance->advanced->excludePosts;
if ( empty( $excludedPosts ) ) {
return false;
}
$ids = [];
foreach ( $excludedPosts as $object ) {
$object = json_decode( $object );
if ( is_int( $object->value ) ) {
$ids[] = (int) $object->value;
}
}
$post = aioseo()->helpers->getPost();
if ( empty( $post ) ) {
return false;
}
if ( in_array( (int) $post->ID, $ids, true ) ) {
return true;
}
return false;
}
/**
* Checks whether the queried object has been excluded globally.
*
* @since 4.0.0
*
* @return bool
*/
protected function isTaxExcludedGlobal() {
if ( ! in_array( 'excludeTerms', aioseo()->internalOptions->deprecatedOptions, true ) ) {
return false;
}
$excludedTerms = aioseo()->options->deprecated->searchAppearance->advanced->excludeTerms;
if ( empty( $excludedTerms ) ) {
return false;
}
$ids = [];
foreach ( $excludedTerms as $object ) {
$object = json_decode( $object );
if ( is_int( $object->value ) ) {
$ids[] = (int) $object->value;
}
}
$term = aioseo()->helpers->getTerm();
if ( in_array( (int) $term->term_id, $ids, true ) ) {
return true;
}
return false;
}
} Meta/Helpers.php 0000666 00000006324 15113050716 0007555 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Meta;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Contains helper methods for the title/description classes.
*
* @since 4.1.2
*/
class Helpers {
use Traits\Helpers\BuddyPress;
/**
* The name of the class where this instance is constructed.
*
* @since 4.1.2
*
* @param string $name The name of the class. Either "title" or "description".
*/
private $name;
/**
* Supported filters we can run after preparing the value.
*
* @since 4.1.2
*
* @var array
*/
private $supportedFilters = [
'title' => 'aioseo_title',
'description' => 'aioseo_description'
];
/**
* Class constructor.
*
* @since 4.1.2
*
* @param string $name The name of the class where this instance is constructed.
*/
public function __construct( $name ) {
$this->name = $name;
}
/**
* Sanitizes the title/description.
*
* @since 4.1.2
*
* @param string $value The value.
* @param int|bool $objectId The post/term ID.
* @param bool $replaceTags Whether the smart tags should be replaced.
* @return string The sanitized value.
*/
public function sanitize( $value, $objectId = false, $replaceTags = false ) {
$value = $replaceTags ? $value : aioseo()->tags->replaceTags( $value, $objectId );
$value = aioseo()->helpers->doShortcodes( $value );
$value = aioseo()->helpers->decodeHtmlEntities( $value );
$value = $this->encodeExceptions( $value );
$value = wp_strip_all_tags( strip_shortcodes( $value ) );
// Because we encoded the exceptions, we need to decode them again first to prevent double encoding later down the line.
$value = aioseo()->helpers->decodeHtmlEntities( $value );
// Trim internal and external whitespace.
$value = preg_replace( '/[\s]+/u', ' ', (string) trim( $value ) );
return aioseo()->helpers->internationalize( $value );
}
/**
* Prepares the title/description before returning it.
*
* @since 4.1.2
*
* @param string $value The value.
* @param int|bool $objectId The post/term ID.
* @param bool $replaceTags Whether the smart tags should be replaced.
* @return string The sanitized value.
*/
public function prepare( $value, $objectId = false, $replaceTags = false ) {
if (
! empty( $value ) &&
! is_admin() &&
1 < aioseo()->helpers->getPageNumber()
) {
$value .= ' ' . trim( aioseo()->options->searchAppearance->advanced->pagedFormat );
}
$value = $replaceTags ? $value : aioseo()->tags->replaceTags( $value, $objectId );
$value = apply_filters( $this->supportedFilters[ $this->name ], $value );
return $this->sanitize( $value, $objectId, $replaceTags );
}
/**
* Encodes a number of exceptions before we strip tags.
* We need this function to allow certain character (combinations) in the title/description.
*
* @since 4.1.1
*
* @param string $string The string.
* @return string $string The string with exceptions encoded.
*/
public function encodeExceptions( $string ) {
$exceptions = [ '<3' ];
foreach ( $exceptions as $exception ) {
$string = preg_replace( "/$exception/", aioseo()->helpers->encodeOutputHtml( $exception ), (string) $string );
}
return $string;
}
} Meta/SiteVerification.php 0000666 00000001650 15113050716 0011417 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Meta;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the site verification meta tags.
*
* @since 4.0.0
*/
class SiteVerification {
/**
* An array of webmaster tools and their meta names.
*
* @since 4.0.0
*
* @var array
*/
private $webmasterTools = [
'google' => 'google-site-verification',
'bing' => 'msvalidate.01',
'pinterest' => 'p:domain_verify',
'yandex' => 'yandex-verification',
'baidu' => 'baidu-site-verification'
];
/**
* Returns the robots meta tag value.
*
* @since 4.0.0
*
* @return mixed The robots meta tag value or false.
*/
public function meta() {
$metaArray = [];
foreach ( $this->webmasterTools as $key => $metaName ) {
$value = aioseo()->options->webmasterTools->$key;
if ( ! empty( $value ) ) {
$metaArray[ $metaName ] = $value;
}
}
return $metaArray;
}
} Meta/Keywords.php 0000666 00000017722 15113050716 0007766 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Meta;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
/**
* Handles the keywords.
*
* @since 4.0.0
*/
class Keywords {
/**
* Get the keywords for the meta output.
*
* @since 4.0.0
*
* @return string The keywords as a string.
*/
public function getKeywords() {
if ( ! aioseo()->options->searchAppearance->advanced->useKeywords ) {
return '';
}
if ( BuddyPressIntegration::isComponentPage() ) {
return aioseo()->standalone->buddyPress->component->getMeta( 'keywords' );
}
$isStaticArchive = aioseo()->helpers->isWooCommerceShopPage() || aioseo()->helpers->isStaticPostsPage();
$dynamicContent = is_archive() || is_post_type_archive() || is_home() || aioseo()->helpers->isWooCommerceShopPage() || is_category() || is_tag() || is_tax();
$generate = aioseo()->options->searchAppearance->advanced->dynamicallyGenerateKeywords;
if ( $dynamicContent && $generate ) {
return $this->prepareKeywords( $this->getGeneratedKeywords() );
}
if ( is_front_page() && ! aioseo()->helpers->isStaticHomePage() ) {
$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->global->keywords );
return $this->prepareKeywords( $keywords );
}
if ( $dynamicContent && ! $isStaticArchive ) {
if ( is_date() ) {
$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->archives->date->advanced->keywords );
return $this->prepareKeywords( $keywords );
}
if ( is_author() ) {
$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->archives->author->advanced->keywords );
return $this->prepareKeywords( $keywords );
}
if ( is_search() ) {
$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->archives->search->advanced->keywords );
return $this->prepareKeywords( $keywords );
}
$postType = get_queried_object();
return is_a( $postType, 'WP_Post_Type' )
? $this->prepareKeywords( $this->getArchiveKeywords( $postType->name ) )
: '';
}
return $this->prepareKeywords( $this->getAllKeywords() );
}
/**
* Retrieves the default keywords for the archive template.
*
* @since 4.7.6
*
* @param string $postType The post type.
* @return array The keywords.
*/
public function getArchiveKeywords( $postType ) {
static $archiveKeywords = [];
if ( isset( $archiveKeywords[ $postType ] ) ) {
return $archiveKeywords[ $postType ];
}
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
if ( $dynamicOptions->searchAppearance->archives->has( $postType ) ) {
$keywords = $this->extractMetaKeywords( aioseo()->dynamicOptions->searchAppearance->archives->{ $postType }->advanced->keywords );
}
$archiveKeywords[ $postType ] = empty( $keywords ) ? [] : $keywords;
return $archiveKeywords[ $postType ];
}
/**
* Get generated keywords for an archive page.
*
* @since 4.0.0
*
* @return array An array of generated keywords.
*/
private function getGeneratedKeywords() {
global $posts, $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
$keywords = [];
$isStaticArchive = aioseo()->helpers->isWooCommerceShopPage() || aioseo()->helpers->isStaticPostsPage();
if ( $isStaticArchive ) {
$keywords = $this->getAllKeywords();
} elseif ( is_front_page() && ! aioseo()->helpers->isStaticHomePage() ) {
$keywords = $this->extractMetaKeywords( aioseo()->options->searchAppearance->global->keywords );
} elseif ( is_category() || is_tag() || is_tax() ) {
$metaData = aioseo()->meta->metaData->getMetaData();
if ( ! empty( $metaData->keywords ) ) {
$keywords = $this->extractMetaKeywords( $metaData->keywords );
}
}
$wpPosts = $posts;
if ( empty( $posts ) ) {
$wpPosts = array_filter( [ aioseo()->helpers->getPost() ] );
}
// Turn off current query so we can get specific post data.
// phpcs:disable Squiz.NamingConventions.ValidVariableName
$originalTag = $wp_query->is_tag;
$originalTax = $wp_query->is_tax;
$originalCategory = $wp_query->is_category;
$wp_query->is_tag = false;
$wp_query->is_tax = false;
$wp_query->is_category = false;
foreach ( $wpPosts as $post ) {
$metaData = aioseo()->meta->metaData->getMetaData( $post );
$tmpKeywords = $this->extractMetaKeywords( $metaData->keywords );
if ( count( $tmpKeywords ) ) {
foreach ( $tmpKeywords as $keyword ) {
$keywords[] = $keyword;
}
}
}
$wp_query->is_tag = $originalTag;
$wp_query->is_tax = $originalTax;
$wp_query->is_category = $originalCategory;
// phpcs:enable Squiz.NamingConventions.ValidVariableName
return $keywords;
}
/**
* Returns the keywords.
*
* @since 4.0.0
*
* @return array A list of unique keywords.
*/
public function getAllKeywords() {
$keywords = [];
$post = aioseo()->helpers->getPost();
$metaData = aioseo()->meta->metaData->getMetaData();
if ( ! empty( $metaData->keywords ) ) {
$keywords = $this->extractMetaKeywords( $metaData->keywords );
}
if ( $post ) {
if ( aioseo()->options->searchAppearance->advanced->useTagsForMetaKeywords ) {
$keywords = array_merge( $keywords, aioseo()->helpers->getAllTags( $post->ID ) );
}
if ( aioseo()->options->searchAppearance->advanced->useCategoriesForMetaKeywords && ! is_page() ) {
$keywords = array_merge( $keywords, aioseo()->helpers->getAllCategories( $post->ID ) );
}
}
return $keywords;
}
/**
* Prepares the keywords for display.
*
* @since 4.0.0
*
* @param array $keywords Raw keywords.
* @return string A list of prepared keywords, comma-separated.
*/
public function prepareKeywords( $keywords ) {
$keywords = $this->getUniqueKeywords( $keywords );
$keywords = trim( $keywords );
$keywords = aioseo()->helpers->internationalize( $keywords );
$keywords = stripslashes( $keywords );
$keywords = str_replace( '"', '', $keywords );
$keywords = wp_filter_nohtml_kses( $keywords );
return apply_filters( 'aioseo_keywords', $keywords );
}
/**
* Returns an array of keywords, based on a stringified list separated by commas.
*
* @since 4.0.0
*
* @param string $keywords The keywords string.
* @return array The keywords.
*/
public function keywordStringToList( $keywords ) {
$keywords = str_replace( '"', '', $keywords );
return ! empty( $keywords ) ? explode( ',', $keywords ) : [];
}
/**
* Returns a stringified list of unique keywords, separated by commas.
*
* @since 4.0.0
*
* @param array $keywords The keywords.
* @param boolean $toString Whether or not to turn it into a comma separated string.
* @return string|array The keywords.
*/
public function getUniqueKeywords( $keywords, $toString = true ) {
$keywords = $this->keywordsToLowerCase( $keywords );
return $toString ? implode( ',', $keywords ) : $keywords;
}
/**
* Returns the keywords in lowercase.
*
* @since 4.0.0
*
* @param array $keywords The keywords.
* @return array The formatted keywords.
*/
private function keywordsToLowerCase( $keywords ) {
$smallKeywords = [];
if ( ! is_array( $keywords ) ) {
$keywords = $this->keywordStringToList( $keywords );
}
if ( ! empty( $keywords ) ) {
foreach ( $keywords as $keyword ) {
$smallKeywords[] = trim( aioseo()->helpers->toLowercase( $keyword ) );
}
}
return array_unique( $smallKeywords );
}
/**
* Extract keywords and then return as a string.
*
* @since 4.0.0
*
* @param array|string $keywords An array of keywords or a json string.
* @return array An array of keywords that were extracted.
*/
public function extractMetaKeywords( $keywords ) {
$extracted = [];
$keywords = is_string( $keywords ) ? json_decode( $keywords ) : $keywords;
if ( ! empty( $keywords ) ) {
foreach ( $keywords as $keyword ) {
$extracted[] = trim( $keyword->value );
}
}
return $extracted;
}
} Meta/Robots.php 0000666 00000026152 15113050716 0007424 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Meta;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
/**
* Handles the robots meta tag.
*
* @since 4.0.0
*/
class Robots {
/**
* The robots meta tag attributes.
*
* We'll already set the keys on construction so that we always output the attributes in the same order.
*
* @since 4.0.0
*
* @var array
*/
protected $attributes = [
'noindex' => '',
'nofollow' => '',
'noarchive' => '',
'nosnippet' => '',
'noimageindex' => '',
'noodp' => '',
'notranslate' => '',
'max-snippet' => '',
'max-image-preview' => '',
'max-video-preview' => ''
];
/**
* Class constructor.
*
* @since 4.0.16
*/
public function __construct() {
add_action( 'wp_loaded', [ $this, 'unregisterWooCommerceNoindex' ] );
add_action( 'template_redirect', [ $this, 'noindexFeed' ] );
add_action( 'wp_head', [ $this, 'disableWpRobotsCore' ], -1 );
}
/**
* Prevents WooCommerce from noindexing the Cart/Checkout pages.
*
* @since 4.1.3
*
* @return void
*/
public function unregisterWooCommerceNoindex() {
if ( has_action( 'wp_head', 'wc_page_noindex' ) ) {
remove_action( 'wp_head', 'wc_page_noindex' );
}
}
/**
* Prevents WP Core from outputting its own robots meta tag.
*
* @since 4.0.16
*
* @return void
*/
public function disableWpRobotsCore() {
remove_all_filters( 'wp_robots' );
}
/**
* Noindexes RSS feed pages.
*
* @since 4.0.17
*
* @return void
*/
public function noindexFeed() {
if (
! is_feed() ||
( ! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default && ! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexFeed )
) {
return;
}
header( 'X-Robots-Tag: noindex, follow', true );
}
/**
* Returns the robots meta tag value.
*
* @since 4.0.0
*
* @return mixed The robots meta tag value or false.
*/
public function meta() {
// We need this check to happen first as spammers can attempt to make the page appear like a post or term by using URL params e.g. "cat=".
if ( is_search() ) {
$this->globalValues( [ 'archives', 'search' ] );
return $this->metaHelper();
}
if ( BuddyPressIntegration::isComponentPage() ) {
return aioseo()->standalone->buddyPress->component->getMeta( 'robots' );
}
if ( is_category() || is_tag() || is_tax() ) {
$this->term();
return $this->metaHelper();
}
if ( is_home() && 'page' !== get_option( 'show_on_front' ) ) {
$this->globalValues();
return $this->metaHelper();
}
$post = aioseo()->helpers->getPost();
if ( $post ) {
$this->post();
return $this->metaHelper();
}
if ( is_author() ) {
$this->globalValues( [ 'archives', 'author' ] );
return $this->metaHelper();
}
if ( is_date() ) {
$this->globalValues( [ 'archives', 'date' ] );
return $this->metaHelper();
}
if ( is_404() ) {
return apply_filters( 'aioseo_404_robots', 'noindex' );
}
if ( is_archive() ) {
$this->archives();
return $this->metaHelper();
}
}
/**
* Stringifies and filters the robots meta tag value.
*
* Acts as a helper for meta().
*
* @since 4.0.0
*
* @param bool $array Whether or not to return the value as an array.
* @return array|string The robots meta tag value.
*/
public function metaHelper( $array = false ) {
$pageNumber = aioseo()->helpers->getPageNumber();
if ( 1 < $pageNumber || aioseo()->helpers->getCommentPageNumber() ) {
if (
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexPaginated
) {
$this->attributes['noindex'] = 'noindex';
}
if (
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->nofollowPaginated
) {
$this->attributes['nofollow'] = 'nofollow';
}
}
// Never allow users to noindex the first page of the homepage.
if ( is_front_page() && 1 === $pageNumber ) {
$this->attributes['noindex'] = '';
}
// Because we prevent WordPress Core from outputting a robots tag in disableWpRobotsCore(), we need to noindex/nofollow non-public sites ourselves.
if ( ! get_option( 'blog_public' ) ) {
$this->attributes['noindex'] = 'noindex';
$this->attributes['nofollow'] = 'nofollow';
}
$this->attributes = array_filter( (array) apply_filters( 'aioseo_robots_meta', $this->attributes ) );
return $array ? $this->attributes : implode( ', ', $this->attributes );
}
/**
* Sets the attributes for the current post.
*
* @since 4.0.0
*
* @param \WP_Post|null $post The post object.
* @return void
*/
public function post( $post = null ) {
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
$post = aioseo()->helpers->getPost( $post );
$metaData = aioseo()->meta->metaData->getMetaData( $post );
if ( ! empty( $metaData ) && ! $metaData->robots_default ) {
$this->metaValues( $metaData );
return;
}
if ( $dynamicOptions->searchAppearance->postTypes->has( $post->post_type ) ) {
$this->globalValues( [ 'postTypes', $post->post_type ], true );
}
}
/**
* Returns the robots meta tag value for the current term.
*
* @since 4.0.6
*
* @param \WP_Term|null $term The term object if any.
* @return void
*/
public function term( $term = null ) {
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
$term = is_a( $term, 'WP_Term' ) ? $term : aioseo()->helpers->getTerm();
// Misbehaving themes/plugins can manipulate the loop and make archives return a post as the queried object.
if ( ! is_a( $term, 'WP_Term' ) ) {
return;
}
if ( $dynamicOptions->searchAppearance->taxonomies->has( $term->taxonomy ) ) {
$this->globalValues( [ 'taxonomies', $term->taxonomy ], true );
return;
}
$this->globalValues();
}
/**
* Sets the attributes for the current archive.
*
* @since 4.0.0
*
* @return void
*/
private function archives() {
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
$postType = aioseo()->helpers->getTerm();
if ( ! empty( $postType->name ) && $dynamicOptions->searchAppearance->archives->has( $postType->name ) ) {
$this->globalValues( [ 'archives', $postType->name ], true );
}
}
/**
* Sets the attributes based on the global values.
*
* @since 4.0.0
*
* @param array $optionOrder The order in which the options need to be called to get the relevant robots meta settings.
* @param boolean $isDynamicOption Whether this is for a dynamic option.
* @return void
*/
public function globalValues( $optionOrder = [], $isDynamicOption = false ) {
$robotsMeta = [];
if ( count( $optionOrder ) ) {
$options = $isDynamicOption
? aioseo()->dynamicOptions->noConflict( true )->searchAppearance
: aioseo()->options->noConflict()->searchAppearance;
foreach ( $optionOrder as $option ) {
if ( ! $options->has( $option, false ) ) {
return;
}
$options = $options->$option;
}
$clonedOptions = clone $options;
if ( ! $clonedOptions->show ) {
$this->attributes['noindex'] = 'noindex';
}
$robotsMeta = $options->advanced->robotsMeta->all();
if ( $robotsMeta['default'] ) {
$robotsMeta = aioseo()->options->searchAppearance->advanced->globalRobotsMeta->all();
}
} else {
$robotsMeta = aioseo()->options->searchAppearance->advanced->globalRobotsMeta->all();
}
$this->attributes['max-image-preview'] = 'max-image-preview:large';
if ( $robotsMeta['default'] ) {
return;
}
if ( $robotsMeta['noindex'] ) {
$this->attributes['noindex'] = 'noindex';
}
if ( $robotsMeta['nofollow'] ) {
$this->attributes['nofollow'] = 'nofollow';
}
if ( $robotsMeta['noarchive'] ) {
$this->attributes['noarchive'] = 'noarchive';
}
$noSnippet = $robotsMeta['nosnippet'];
if ( $noSnippet ) {
$this->attributes['nosnippet'] = 'nosnippet';
}
if ( $robotsMeta['noodp'] ) {
$this->attributes['noodp'] = 'noodp';
}
if ( $robotsMeta['notranslate'] ) {
$this->attributes['notranslate'] = 'notranslate';
}
$maxSnippet = $robotsMeta['maxSnippet'];
if ( ! $noSnippet && is_numeric( $maxSnippet ) ) {
$this->attributes['max-snippet'] = "max-snippet:$maxSnippet";
}
$maxImagePreview = $robotsMeta['maxImagePreview'];
$noImageIndex = $robotsMeta['noimageindex'];
if ( ! $noImageIndex && $maxImagePreview && in_array( $maxImagePreview, [ 'none', 'standard', 'large' ], true ) ) {
$this->attributes['max-image-preview'] = "max-image-preview:$maxImagePreview";
}
$maxVideoPreview = $robotsMeta['maxVideoPreview'];
if ( isset( $maxVideoPreview ) && is_numeric( $maxVideoPreview ) ) {
$this->attributes['max-video-preview'] = "max-video-preview:$maxVideoPreview";
}
// Check this last so that we can prevent max-image-preview from being output if noimageindex is enabled.
if ( $noImageIndex ) {
$this->attributes['max-image-preview'] = '';
$this->attributes['noimageindex'] = 'noimageindex';
}
}
/**
* Sets the attributes from the meta data.
*
* @since 4.0.0
*
* @param \AIOSEO\Plugin\Common\Models\Post|\AIOSEO\Plugin\Pro\Models\Term $metaData The post/term meta data.
* @return void
*/
protected function metaValues( $metaData ) {
if ( $metaData->robots_noindex || $this->isPasswordProtected() ) {
$this->attributes['noindex'] = 'noindex';
}
if ( $metaData->robots_nofollow ) {
$this->attributes['nofollow'] = 'nofollow';
}
if ( $metaData->robots_noarchive ) {
$this->attributes['noarchive'] = 'noarchive';
}
if ( $metaData->robots_nosnippet ) {
$this->attributes['nosnippet'] = 'nosnippet';
}
if ( $metaData->robots_noodp ) {
$this->attributes['noodp'] = 'noodp';
}
if ( $metaData->robots_notranslate ) {
$this->attributes['notranslate'] = 'notranslate';
}
if ( ! $metaData->robots_nosnippet && isset( $metaData->robots_max_snippet ) && is_numeric( $metaData->robots_max_snippet ) ) {
$this->attributes['max-snippet'] = "max-snippet:$metaData->robots_max_snippet";
}
if ( ! $metaData->robots_noimageindex && $metaData->robots_max_imagepreview && in_array( $metaData->robots_max_imagepreview, [ 'none', 'standard', 'large' ], true ) ) {
$this->attributes['max-image-preview'] = "max-image-preview:$metaData->robots_max_imagepreview";
}
if ( isset( $metaData->robots_max_videopreview ) && is_numeric( $metaData->robots_max_videopreview ) ) {
$this->attributes['max-video-preview'] = "max-video-preview:$metaData->robots_max_videopreview";
}
// Check this last so that we can prevent max-image-preview from being output if noimageindex is enabled.
if ( $metaData->robots_noimageindex ) {
$this->attributes['max-image-preview'] = '';
$this->attributes['noimageindex'] = 'noimageindex';
}
}
/**
* Checks whether the current post is password protected.
*
* @since 4.0.0
*
* @return bool Whether the post is password protected.
*/
private function isPasswordProtected() {
$post = aioseo()->helpers->getPost();
return is_object( $post ) && $post->post_password;
}
} Meta/Title.php 0000666 00000015461 15113050716 0007236 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Meta;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
/**
* Handles the title.
*
* @since 4.0.0
*/
class Title {
/**
* Helpers class instance.
*
* @since 4.2.7
*
* @var Helpers
*/
public $helpers = null;
/**
* Class constructor.
*
* @since 4.1.2
*/
public function __construct() {
$this->helpers = new Helpers( 'title' );
}
/**
* Returns the filtered page title.
*
* Acts as a helper for getTitle() because we need to encode the title before sending it back to the filter.
*
* @since 4.0.0
*
* @return string The page title.
*/
public function filterPageTitle( $wpTitle = '' ) {
$title = $this->getTitle();
return ! empty( $title ) ? aioseo()->helpers->encodeOutputHtml( $title ) : $wpTitle;
}
/**
* Returns the homepage title.
*
* @since 4.0.0
*
* @return string The homepage title.
*/
public function getHomePageTitle() {
if ( 'page' === get_option( 'show_on_front' ) ) {
$title = $this->getPostTitle( (int) get_option( 'page_on_front' ) );
return $title ? $title : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
}
$title = aioseo()->options->searchAppearance->global->siteTitle;
if ( aioseo()->helpers->isWpmlActive() ) {
// Allow WPML to translate the title if the homepage is not static.
$title = apply_filters( 'wpml_translate_single_string', $title, 'admin_texts_aioseo_options_localized', '[aioseo_options_localized]searchAppearance_global_siteTitle' );
}
$title = $this->helpers->prepare( $title );
return $title ? $title : aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
}
/**
* Returns the title for the current page.
*
* @since 4.0.0
*
* @param \WP_Post $post The post object (optional).
* @param boolean $default Whether we want the default value, not the post one.
* @return string The page title.
*/
public function getTitle( $post = null, $default = false ) {
if ( BuddyPressIntegration::isComponentPage() ) {
return aioseo()->standalone->buddyPress->component->getMeta( 'title' );
}
if ( is_home() ) {
return $this->getHomePageTitle();
}
if ( $post || is_singular() || aioseo()->helpers->isStaticPage() ) {
return $this->getPostTitle( $post, $default );
}
if ( is_category() || is_tag() || is_tax() ) {
$term = $post ? $post : aioseo()->helpers->getTerm();
return $this->getTermTitle( $term, $default );
}
if ( is_author() ) {
return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->author->title );
}
if ( is_date() ) {
return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->date->title );
}
if ( is_search() ) {
return $this->helpers->prepare( aioseo()->options->searchAppearance->archives->search->title );
}
if ( is_post_type_archive() ) {
$postType = get_queried_object();
if ( is_a( $postType, 'WP_Post_Type' ) ) {
return $this->helpers->prepare( $this->getArchiveTitle( $postType->name ) );
}
}
return '';
}
/**
* Returns the post title.
*
* @since 4.0.0
*
* @param \WP_Post|int $post The post object or ID.
* @param boolean $default Whether we want the default value, not the post one.
* @return string The post title.
*/
public function getPostTitle( $post, $default = false ) {
$post = $post && is_object( $post ) ? $post : aioseo()->helpers->getPost( $post );
if ( ! is_a( $post, 'WP_Post' ) ) {
return '';
}
static $posts = [];
if ( isset( $posts[ $post->ID ] ) ) {
return $posts[ $post->ID ];
}
$title = '';
$metaData = aioseo()->meta->metaData->getMetaData( $post );
if ( ! empty( $metaData->title ) && ! $default ) {
$title = $this->helpers->prepare( $metaData->title, $post->ID );
}
if ( ! $title ) {
$title = $this->helpers->prepare( $this->getPostTypeTitle( $post->post_type ), $post->ID, $default );
}
// If this post is the static home page and we have no title, let's reset to the site name.
if ( empty( $title ) && 'page' === get_option( 'show_on_front' ) && (int) get_option( 'page_on_front' ) === $post->ID ) {
$title = aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) );
}
if ( empty( $title ) ) {
// Just return the WP default.
$title = get_the_title( $post->ID ) . ' - ' . get_bloginfo( 'name' );
$title = aioseo()->helpers->decodeHtmlEntities( $title );
}
$posts[ $post->ID ] = $title;
return $posts[ $post->ID ];
}
/**
* Retrieve the default title for the archive template.
*
* @since 4.7.6
*
* @param string $postType The custom post type.
* @return string The title.
*/
public function getArchiveTitle( $postType ) {
static $archiveTitle = [];
if ( isset( $archiveTitle[ $postType ] ) ) {
return $archiveTitle[ $postType ];
}
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
if ( $dynamicOptions->searchAppearance->archives->has( $postType ) ) {
$title = aioseo()->dynamicOptions->searchAppearance->archives->{ $postType }->title;
}
$archiveTitle[ $postType ] = empty( $title ) ? '' : $title;
return $archiveTitle[ $postType ];
}
/**
* Retrieve the default title for the post type.
*
* @since 4.0.6
*
* @param string $postType The post type.
* @return string The title.
*/
public function getPostTypeTitle( $postType ) {
static $postTypeTitle = [];
if ( isset( $postTypeTitle[ $postType ] ) ) {
return $postTypeTitle[ $postType ];
}
if ( aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
$title = aioseo()->dynamicOptions->searchAppearance->postTypes->{$postType}->title;
}
$postTypeTitle[ $postType ] = empty( $title ) ? '' : $title;
return $postTypeTitle[ $postType ];
}
/**
* Returns the term title.
*
* @since 4.0.6
*
* @param \WP_Term $term The term object.
* @param boolean $default Whether we want the default value, not the post one.
* @return string The term title.
*/
public function getTermTitle( $term, $default = false ) {
if ( ! is_a( $term, 'WP_Term' ) ) {
return '';
}
static $terms = [];
if ( isset( $terms[ $term->term_id ] ) ) {
return $terms[ $term->term_id ];
}
$title = '';
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
if ( ! $title && $dynamicOptions->searchAppearance->taxonomies->has( $term->taxonomy ) ) {
$newTitle = aioseo()->dynamicOptions->searchAppearance->taxonomies->{$term->taxonomy}->title;
$newTitle = preg_replace( '/#taxonomy_title/', aioseo()->helpers->escapeRegexReplacement( $term->name ), (string) $newTitle );
$title = $this->helpers->prepare( $newTitle, $term->term_id, $default );
}
$terms[ $term->term_id ] = $title;
return $terms[ $term->term_id ];
}
} Meta/Traits/Helpers/BuddyPress.php 0000666 00000001370 15113050716 0013103 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Meta\Traits\Helpers;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Contains BuddyPress specific helper methods.
*
* @since 4.7.6
*/
trait BuddyPress {
/**
* Sanitizes the title/description.
*
* @since 4.7.6
*
* @param string $value The value.
* @param int $objectId The object ID.
* @param bool $replaceTags Whether the smart tags should be replaced.
* @return string The sanitized value.
*/
public function bpSanitize( $value, $objectId = 0, $replaceTags = false ) {
$value = $replaceTags ? $value : aioseo()->standalone->buddyPress->tags->replaceTags( $value, $objectId );
return $this->sanitize( $value, $objectId, true );
}
} Meta/Links.php 0000666 00000011442 15113050716 0007230 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Meta;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Instantiates the meta links "next" and "prev".
*
* @since 4.0.0
*/
class Links {
/**
* Get the prev/next links for the current page.
*
* @since 4.0.0
*
* @return array An array of link data.
*/
public function getLinks() {
$links = [
'prev' => '',
'next' => '',
];
if ( is_home() || is_archive() || is_paged() ) {
$links = $this->getHomeLinks();
}
if ( is_page() || is_single() ) {
global $post;
$links = $this->getPostLinks( $post );
}
$links['prev'] = apply_filters( 'aioseo_prev_link', $links['prev'] );
$links['next'] = apply_filters( 'aioseo_next_link', $links['next'] );
return $links;
}
/**
* Get the prev/next links for the current page (home/archive, etc.).
*
* @since 4.0.0
*
* @return array An array of link data.
*/
private function getHomeLinks() {
$prev = '';
$next = '';
$page = aioseo()->helpers->getPageNumber();
global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
$maxPage = $wp_query->max_num_pages; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
if ( $page > 1 ) {
$prev = get_previous_posts_page_link();
}
if ( $page < $maxPage ) {
$next = get_next_posts_page_link();
$paged = is_paged();
if ( ! is_single() ) {
if ( ! $paged ) {
$page = 1;
}
$nextpage = intval( $page ) + 1;
if ( ! $maxPage || $maxPage >= $nextpage ) {
$next = get_pagenum_link( $nextpage );
}
}
}
// Remove trailing slashes if not set in the permalink structure.
$prev = aioseo()->helpers->maybeRemoveTrailingSlash( $prev );
$next = aioseo()->helpers->maybeRemoveTrailingSlash( $next );
// Remove any query args that may be set on the URL, except if the site is using plain permalinks.
$permalinkStructure = get_option( 'permalink_structure' );
if ( ! empty( $permalinkStructure ) ) {
$prev = explode( '?', $prev )[0];
$next = explode( '?', $next )[0];
}
return [
'prev' => $prev,
'next' => $next,
];
}
/**
* Get the prev/next links for the current post.
*
* @since 4.0.0
*
* @param \WP_Post $post The post.
* @return array An array of link data.
*/
private function getPostLinks( $post ) {
$prev = '';
$next = '';
$numpages = 1;
$page = aioseo()->helpers->getPageNumber();
$content = is_a( $post, 'WP_Post' ) ? $post->post_content : '';
if ( false !== strpos( $content, '<!--nextpage-->', 0 ) ) {
$content = str_replace( "\n<!--nextpage-->\n", '<!--nextpage-->', $content );
$content = str_replace( "\n<!--nextpage-->", '<!--nextpage-->', $content );
$content = str_replace( "<!--nextpage-->\n", '<!--nextpage-->', $content );
// Ignore nextpage at the beginning of the content.
if ( 0 === strpos( $content, '<!--nextpage-->', 0 ) ) {
$content = substr( $content, 15 );
}
$pages = explode( '<!--nextpage-->', $content );
$numpages = count( $pages );
} else {
$page = null;
}
if ( ! empty( $page ) ) {
if ( $page > 1 ) {
$prev = $this->getLinkPage( $page - 1 );
}
if ( $page + 1 <= $numpages ) {
$next = $this->getLinkPage( $page + 1 );
}
}
return [
'prev' => $prev,
'next' => $next,
];
}
/**
* This is a clone of _wp_link_page, except that we don't output HTML.
*
* @since 4.0.0
*
* @param integer $number The page number.
* @return string The URL.
*/
private function getLinkPage( $number ) {
global $wp_rewrite; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
$post = get_post();
$queryArgs = [];
if ( 1 === (int) $number ) {
$url = get_permalink();
} else {
if ( ! get_option( 'permalink_structure' ) || in_array( $post->post_status, [ 'draft', 'pending' ], true ) ) {
$url = add_query_arg( 'page', $number, get_permalink() );
} elseif ( 'page' === get_option( 'show_on_front' ) && get_option( 'page_on_front' ) === $post->ID ) {
$url = trailingslashit( get_permalink() ) . user_trailingslashit( "$wp_rewrite->pagination_base/" . $number, 'single_paged' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
} else {
$url = trailingslashit( get_permalink() ) . user_trailingslashit( $number, 'single_paged' );
}
}
if ( is_preview() ) {
// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
if ( ( 'draft' !== $post->post_status ) && isset( $_GET['preview_id'], $_GET['preview_nonce'] ) ) {
$queryArgs['preview_id'] = sanitize_text_field( wp_unslash( $_GET['preview_id'] ) );
$queryArgs['preview_nonce'] = sanitize_text_field( wp_unslash( $_GET['preview_nonce'] ) );
}
// phpcs:enable
$url = get_preview_post_link( $post, $queryArgs, $url );
}
return esc_url( $url );
}
} QueryArgs/CrawlCleanup.php 0000666 00000024060 15113050716 0011564 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\QueryArgs;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Class to control Crawl Cleanup.
*
* @since 4.5.8
*/
class CrawlCleanup {
/**
* Construct method.
*
* @since 4.5.8
*/
public function __construct() {
// Add action to clear crawl cleanup logs.
add_action( 'aioseo_crawl_cleanup_clear_logs', [ $this, 'clearLogs' ] );
if ( aioseo()->options->searchAppearance->advanced->blockArgs->optimizeUtmParameters ) {
add_action( 'template_redirect', [ $this, 'maybeRedirectUtmParameters' ], 50 );
}
}
/**
* Redirects the UTM parameters to with (#) equivalent.
*
* @since 4.8.0
*
* @return void
*/
public function maybeRedirectUtmParameters() {
$requestUri = aioseo()->helpers->getRequestUrl();
if ( empty( $requestUri ) ) {
return;
}
$parsed = wp_parse_url( $requestUri );
if ( empty( $parsed['query'] ) ) {
return;
}
$args = [];
wp_parse_str( $parsed['query'], $args );
// Reset query to reconstruct without utm_ parameters.
$parsed['query'] = '';
// Initialize the fragment key if it's not set.
if ( ! isset( $parsed['fragment'] ) ) {
$parsed['fragment'] = '';
}
// Check if there are any utm_ parameters and redirect accordingly.
$utmFound = false;
foreach ( $args as $key => $value ) {
$keyValue = $key . '=' . $value;
if ( 0 === stripos( $key, 'utm_' ) ) {
$utmFound = true;
// Rebuild the URL with # instead of ?.
$parsed['fragment'] .= ! empty( $parsed['fragment'] ) ? '&' . $keyValue : $keyValue;
} else {
$parsed['query'] .= ! empty( $parsed['query'] ) ? '&' . $keyValue : $keyValue;
}
}
if ( $utmFound ) {
aioseo()->helpers->redirect( aioseo()->helpers->buildUrl( $parsed ), 301, 'Optimize UTM parameters' );
}
}
/**
* Schedule clearing of the logs.
*
* @since 4.5.8
*
* @return void
*/
public function scheduleClearingLogs() {
aioseo()->actionScheduler->unschedule( 'aioseo_crawl_cleanup_clear_logs' );
$optionLength = json_decode( aioseo()->options->searchAppearance->advanced->blockArgs->logsRetention )->value;
if (
aioseo()->options->searchAppearance->advanced->blockArgs->enable &&
'forever' !== $optionLength
) {
aioseo()->actionScheduler->scheduleRecurrent( 'aioseo_crawl_cleanup_clear_logs', 0, HOUR_IN_SECONDS );
}
}
/**
* Clears the logs.
*
* @since 4.5.8
*
* @return void
*/
public function clearLogs() {
$optionLength = json_decode( aioseo()->options->searchAppearance->advanced->blockArgs->logsRetention )->value;
if ( 'forever' === $optionLength ) {
return;
}
$date = gmdate( 'Y-m-d H:i:s', strtotime( '-1 ' . $optionLength ) );
aioseo()->core->db
->delete( 'aioseo_crawl_cleanup_logs' )
->where( 'updated <', $date )
->run();
}
/**
* Fetch Crawl Cleanup Logs.
*
* @since 4.5.8
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function fetchLogs( $request ) {
$filter = $request->get_param( 'filter' );
$body = $request->get_json_params();
$orderByUnblocked = ! empty( $body['orderBy'] ) ? sanitize_text_field( $body['orderBy'] ) : 'logs.updated';
$orderByBlocked = ! empty( $body['orderBy'] ) ? sanitize_text_field( $body['orderBy'] ) : 'b.id';
$orderDir = ! empty( $body['orderDir'] ) && ! empty( $body['orderBy'] ) ? strtoupper( sanitize_text_field( $body['orderDir'] ) ) : 'DESC';
$limit = ! empty( $body['limit'] ) ? intval( $body['limit'] ) : aioseo()->settings->tablePagination['queryArgs'];
$offset = ! empty( $body['offset'] ) ? intval( $body['offset'] ) : 0;
$searchTerm = ! empty( $body['searchTerm'] ) ? sanitize_text_field( $body['searchTerm'] ) : null;
$keyValueSeparator = Models\CrawlCleanupBlockedArg::getKeyValueSeparator();
$dateFormat = get_option( 'date_format' );
$timeFormat = get_option( 'time_format' );
$dateTimeFormat = $dateFormat . ' ' . $timeFormat;
// Query to get Arg Logs (unblocked) and the total.
$queryUnblocked = aioseo()->core->db
->start( 'aioseo_crawl_cleanup_logs as logs' )
->select( ' logs.id,
logs.slug,
logs.key,
logs.value,
logs.hits,
logs.updated' )
->leftJoin( 'aioseo_crawl_cleanup_blocked_args as blocked',
'blocked.key_value_hash = sha1(logs.key) OR
blocked.key_value_hash = sha1(concat(logs.key, "' . $keyValueSeparator . '", logs.value))' )
->limit( $limit, $offset );
if ( ! empty( $searchTerm ) ) {
// Apply escape to the search term.
$searchTerm = esc_sql( aioseo()->core->db->db->esc_like( $searchTerm ) );
$where = '
(
logs.slug LIKE \'%' . $searchTerm . '%\' OR
logs.slug LIKE \'%' . str_replace( '%20', '-', $searchTerm ) . '%\' OR
logs.slug LIKE \'%' . str_replace( '%20', '+', $searchTerm ) . '%\'
)
';
$queryUnblocked->whereRaw( $where );
}
$queryUnblocked->where( 'blocked.id', null );
$queryUnblocked->orderBy( "$orderByUnblocked $orderDir" );
$rowsUnblocked = $queryUnblocked->run( false )->result();
$totalUnblocked = $queryUnblocked->reset( [ 'limit' ] )->count();
// Test logs (unblocked) to see if have some regex block.
$regexMatches = [];
foreach ( $rowsUnblocked as $unblocked ) {
$blockedRegex = Models\CrawlCleanupBlockedArg::matchRegex( $unblocked->key, $unblocked->value );
if ( $blockedRegex->exists() ) {
$regexMatches[ $unblocked->id ] = $blockedRegex->regex;
}
}
// Query to get Blocked Args and the total.
$queryBlocked = aioseo()->core->db
->select( ' b.id,
b.key,
b.value,
b.regex,
b.hits,
b.updated' )
->start( 'aioseo_crawl_cleanup_blocked_args as b' )
->limit( $limit, $offset );
if ( ! empty( $searchTerm ) ) {
// Escape (esc_like) has already been applied.
$searchTerms = [
$searchTerm,
str_replace( '%20', '-', $searchTerm ),
str_replace( '%20', '+', $searchTerm )
];
$comparisons = [
'b.key',
'b.value',
'b.regex',
'CONCAT(b.key, \'' . $keyValueSeparator . '\', IF(b.value, b.value, \'*\'))'
];
$where = '';
foreach ( $comparisons as $comparison ) {
foreach ( $searchTerms as $s ) {
if ( ! empty( $where ) ) {
$where .= ' OR ';
}
$where .= aioseo()->db->db->prepare( " $comparison LIKE %s ", '%' . $s . '%' );
}
}
$where = "( $where )";
$queryBlocked->whereRaw( $where );
}
$queryBlocked->orderBy( "$orderByBlocked $orderDir" );
$rowsBlocked = $queryBlocked->run( false )->result();
$totalBlocked = $queryBlocked->reset( [ 'limit' ] )->count();
switch ( $filter ) {
case 'blocked':
$total = $totalBlocked;
$rows = $rowsBlocked;
break;
case 'unblocked':
$total = $totalUnblocked;
$rows = $rowsUnblocked;
break;
default:
return new \WP_REST_Response( [
'success' => false
], 404 );
}
foreach ( $rows as $row ) {
$row->updated = get_date_from_gmt( $row->updated, $dateTimeFormat );
}
return new \WP_REST_Response( [
'success' => true,
'rows' => $rows,
'regex' => $regexMatches,
'totals' => [
'total' => $total,
'pages' => 0 === $total ? 1 : ceil( $total / $limit ),
'page' => 0 === $offset ? 1 : ( $offset / $limit ) + 1
],
'filters' => [
[
'slug' => 'unblocked',
'name' => __( 'Unblocked', 'all-in-one-seo-pack' ),
'count' => $totalUnblocked,
'active' => 'unblocked' === $filter
],
[
'slug' => 'blocked',
'name' => __( 'Blocked', 'all-in-one-seo-pack' ),
'count' => $totalBlocked,
'active' => 'blocked' === $filter
]
]
], 200 );
}
/**
* Set block Arg Query.
*
* @since 4.5.8
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function blockArg( $request ) {
$body = $request->get_json_params();
$return = true;
$listSaved = [];
$exists = [];
$error = 0;
try {
foreach ( $body as $block ) {
if ( $block ) {
$blocked = Models\CrawlCleanupBlockedArg::getByKeyValue( $block['key'], $block['value'] );
if ( ! $blocked->exists() && ! empty( $block['regex'] ) ) {
$blocked = Models\CrawlCleanupBlockedArg::getByRegex( $block['regex'] );
}
if ( $blocked->exists() ) {
$exists[] = [
'key' => $block['key'],
'value' => $block['value']
];
$keyValue = sha1( Models\CrawlCleanupBlockedArg::getKeyValueString( $block['key'], $block['value'] ) );
if ( ! in_array( $keyValue, $listSaved, true ) ) {
$return = false;
$error = 1;
}
continue;
}
$blocked = new Models\CrawlCleanupBlockedArg();
$blocked->set( $block );
$blocked->save();
$listSaved[] = $blocked->key_value_hash;
}
}
} catch ( \Throwable $th ) {
$return = false;
}
return new \WP_REST_Response( [
'success' => $return,
'error' => $error,
'exists' => $exists
], 200 );
}
/**
* Delete Blocked Arg.
*
* @since 4.5.8
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function deleteBlocked( $request ) {
$body = $request->get_json_params();
$return = true;
try {
foreach ( $body as $block ) {
$blocked = new Models\CrawlCleanupBlockedArg( $block );
if ( $blocked->exists() ) {
$blocked->delete();
}
}
} catch ( \Throwable $th ) {
$return = false;
}
return new \WP_REST_Response( [
'success' => $return
], 200 );
}
/**
* Delete Log.
*
* @since 4.5.8
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function deleteLog( $request ) {
$body = $request->get_json_params();
$return = true;
try {
foreach ( $body as $block ) {
$log = new Models\CrawlCleanupLog( $block );
if ( $log->exists() ) {
$log->delete();
}
}
} catch ( \Throwable $th ) {
$return = false;
}
return new \WP_REST_Response( [
'success' => $return
], 200 );
}
} Breadcrumbs/Frontend.php 0000666 00000021055 15113050716 0011273 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
/**
* Class Frontend.
*
* @since 4.1.1
*/
class Frontend {
/**
* A local 'cached' crumb array.
*
* @since 4.1.1
*
* @var array
*/
public $breadcrumbs = [];
/**
* Gets the current page's breadcrumbs.
*
* @since 4.1.1
*
* @return array
*/
public function getBreadcrumbs() {
if ( ! empty( $this->breadcrumbs ) ) {
return apply_filters( 'aioseo_breadcrumbs_trail', $this->breadcrumbs );
}
$reference = get_queried_object();
$type = '';
if ( BuddyPressIntegration::isComponentPage() ) {
$type = 'buddypress';
}
if ( ! $type ) {
// These types need the queried object for reference.
if ( is_object( $reference ) ) {
if ( is_single() ) {
$type = 'single';
}
if ( is_singular( 'post' ) ) {
$type = 'post';
}
if ( is_page() && ! is_front_page() ) {
$type = 'page';
}
if ( is_category() || is_tag() ) {
$type = 'category';
}
if ( is_tax() ) {
$type = 'taxonomy';
}
if ( is_post_type_archive() ) {
$type = 'postTypeArchive';
}
if ( is_author() ) {
$type = 'author';
}
if ( is_home() ) {
$type = 'blog';
}
// Support WC shop page.
if ( aioseo()->helpers->isWooCommerceShopPage() ) {
$type = 'wcShop';
}
// Support WC products.
if ( aioseo()->helpers->isWooCommerceProductPage() ) {
$type = 'wcProduct';
}
}
if ( is_date() ) {
$type = 'date';
$reference = [
'year' => get_query_var( 'year' ),
'month' => get_query_var( 'monthnum' ),
'day' => get_query_var( 'day' )
];
}
if ( is_search() ) {
$type = 'search';
$reference = htmlspecialchars( sanitize_text_field( get_search_query() ) );
}
if ( is_404() ) {
$type = 'notFound';
}
}
$paged = false;
if ( is_paged() || ( is_singular() && 1 < get_query_var( 'page' ) ) ) {
global $wp;
$paged = [
'paged' => get_query_var( 'paged' ) ? get_query_var( 'paged' ) : get_query_var( 'page' ),
'link' => home_url( $wp->request )
];
}
return apply_filters( 'aioseo_breadcrumbs_trail', aioseo()->breadcrumbs->buildBreadcrumbs( $type, $reference, $paged ) );
}
/**
* Helper function to display breadcrumbs for a specific page.
*
* @since 4.1.1
*
* @param bool $echo Print out the breadcrumb.
* @param string $type The type for the breadcrumb.
* @param string $reference A reference to be used for rendering the breadcrumb.
* @return string|void A html breadcrumb.
*/
public function sideDisplay( $echo = true, $type = '', $reference = '' ) {
// Save previously built breadcrumbs.
$previousCrumbs = $this->breadcrumbs;
// Build and run the sideDisplay.
$this->breadcrumbs = aioseo()->breadcrumbs->buildBreadcrumbs( $type, $reference );
$sideDisplay = $this->display( $echo );
// Restore previously built breadcrumbs.
$this->breadcrumbs = $previousCrumbs;
return $sideDisplay;
}
/**
* Display a generic breadcrumb preview.
*
* @since 4.1.5
*
* @param bool $echo Print out the breadcrumb.
* @param string $label The preview crumb label.
* @return string|void A html breadcrumb.
*/
public function preview( $echo = true, $label = '' ) {
// Translators: "Crumb" refers to a part of the breadcrumb trail.
$label = empty( $label ) ? __( 'Sample Crumb', 'all-in-one-seo-pack' ) : $label;
return $this->sideDisplay( $echo, 'preview', $label );
}
/**
* Display the breadcrumb in the frontend.
*
* @since 4.1.1
*
* @param bool $echo Print out the breadcrumb.
* @return string|void A html breadcrumb.
*/
public function display( $echo = true ) {
if (
in_array( 'breadcrumbsEnable', aioseo()->internalOptions->deprecatedOptions, true ) &&
! aioseo()->options->deprecated->breadcrumbs->enable
) {
return;
}
if ( ! apply_filters( 'aioseo_breadcrumbs_output', true ) ) {
return;
}
// We can only run after this action because we need all post types loaded.
if ( ! did_action( 'init' ) ) {
return;
}
$breadcrumbs = $this->getBreadcrumbs();
if ( empty( $breadcrumbs ) ) {
return;
}
$breadcrumbsCount = count( $breadcrumbs );
$display = '<div class="aioseo-breadcrumbs">';
foreach ( $breadcrumbs as $breadcrumb ) {
--$breadcrumbsCount;
$breadcrumbDisplay = $this->breadcrumbToDisplay( $breadcrumb );
// Strip link from Last crumb.
if (
0 === $breadcrumbsCount &&
aioseo()->breadcrumbs->showCurrentItem() &&
! $this->linkCurrentItem() &&
'default' === $breadcrumbDisplay['templateType']
) {
$breadcrumbDisplay['template'] = $this->stripLink( $breadcrumbDisplay['template'] );
}
$display .= $breadcrumbDisplay['template'];
if ( 0 < $breadcrumbsCount ) {
$display .= $this->getSeparator();
}
}
$display .= '</div>';
// Final security cleaning.
$display = wp_kses_post( $display );
if ( $echo ) {
echo $display; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
return $display;
}
/**
* Turns a crumb array into a rendered html crumb.
*
* @since 4.1.1
*
* @param array $item The crumb array.
* @return string|void The crumb html.
*/
protected function breadcrumbToDisplay( $item ) {
$templateItem = $this->getCrumbTemplate( $item );
if ( empty( $templateItem['template'] ) ) {
return;
}
// Do tags.
$templateItem['template'] = aioseo()->breadcrumbs->tags->replaceTags( $templateItem['template'], $item );
$templateItem['template'] = preg_replace_callback(
'/>(?![^<]*>)(?![^>]*")([^<]*?)>/',
function ( $matches ) {
return '>' . $matches[1] . '>';
},
htmlentities( $templateItem['template'] )
);
// Restore html.
$templateItem['template'] = aioseo()->helpers->decodeHtmlEntities( $templateItem['template'] );
// Remove html link if it comes back from the template but we passed no links to it.
if ( empty( $item['link'] ) ) {
$templateItem['template'] = $this->stripLink( $templateItem['template'] );
}
// Allow shortcodes to run in the final html.
$templateItem['template'] = do_shortcode( $templateItem['template'] );
return $templateItem;
}
/**
* Helper function to get a crumb's template.
*
* @since 4.1.1
*
* @param array $crumb The crumb array.
* @return string The html template.
*/
protected function getTemplate( $crumb ) {
return $this->getDefaultTemplate( $crumb );
}
/**
* Helper function to get a crumb's template.
*
* @since 4.1.1
*
* @param array $crumb The crumb array.
* @return array The template type and html.
*/
protected function getCrumbTemplate( $crumb ) {
return [
'templateType' => 'default',
'template' => $this->getTemplate( $crumb )
];
}
/**
* Default html template.
*
* @since 4.1.1
*
* @param string $type The crumb's type.
* @param mixed $reference The crumb's reference.
* @return string The default crumb template.
*/
public function getDefaultTemplate( $type = '', $reference = '' ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return <<<TEMPLATE
<span class="aioseo-breadcrumb">
<a href="#breadcrumb_link" title="#breadcrumb_label">#breadcrumb_label</a>
</span>
TEMPLATE;
}
/**
* Helper function to strip a html link from the crumb.
*
* @since 4.1.1
*
* @param string $html The crumb's html.
* @return string A crumb html without links.
*/
public function stripLink( $html ) {
return preg_replace( '/<a\s.*?>|<\/a>/is', '', (string) $html );
}
/**
* Get the breadcrumb configured separator.
*
* @since 4.1.1
*
* @return string The separator html.
*/
public function getSeparator() {
$separator = aioseo()->options->breadcrumbs->separator;
$separatorToOverride = aioseo()->breadcrumbs->getOverride( 'separator' );
if ( ! empty( $separatorToOverride ) ) {
$separator = $separatorToOverride;
}
$separator = apply_filters( 'aioseo_breadcrumbs_separator_symbol', $separator );
return apply_filters( 'aioseo_breadcrumbs_separator', '<span class="aioseo-breadcrumb-separator">' . esc_html( $separator ) . '</span>' );
}
/**
* Function to filter the linkCurrentItem option.
*
* @since 4.1.3
*
* @return bool Link current item.
*/
public function linkCurrentItem() {
return apply_filters( 'aioseo_breadcrumbs_link_current_item', aioseo()->options->breadcrumbs->linkCurrentItem );
}
} Breadcrumbs/Shortcode.php 0000666 00000001037 15113050716 0011444 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Shortcode.
*
* @since 4.1.1
*/
class Shortcode {
/**
* Shortcode constructor.
*
* @since 4.1.1
*/
public function __construct() {
add_shortcode( 'aioseo_breadcrumbs', [ $this, 'display' ] );
}
/**
* Shortcode callback.
*
* @since 4.1.1
*
* @return string|void The breadcrumb html.
*/
public function display() {
return aioseo()->breadcrumbs->frontend->display( false );
}
} Breadcrumbs/Widget.php 0000666 00000006410 15113050716 0010735 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Widget.
*
* @since 4.1.1
*/
class Widget extends \WP_Widget {
/**
* The default attributes.
*
* @since 4.2.7
*
* @var array
*/
private $defaults = [];
/**
* Class constructor.
*
* @since 4.1.1
*/
public function __construct() {
// Widget defaults.
$this->defaults = [
'title' => ''
];
// Widget Slug.
$widgetSlug = 'aioseo-breadcrumb-widget';
// Widget basics.
$widgetOps = [
'classname' => $widgetSlug,
'description' => esc_html__( 'Display the current page breadcrumb.', 'all-in-one-seo-pack' ),
];
// Widget controls.
$controlOps = [
'id_base' => $widgetSlug,
];
// Translators: 1 - The plugin short name ("AIOSEO").
$name = sprintf( esc_html__( '%1$s - Breadcrumbs', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );
$name .= ' ' . esc_html__( '(legacy)', 'all-in-one-seo-pack' );
parent::__construct( $widgetSlug, $name, $widgetOps, $controlOps );
}
/**
* Widget callback.
*
* @since 4.1.1
*
* @param array $args Widget args.
* @param array $instance The widget instance options.
* @return void
*/
public function widget( $args, $instance ) {
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
// Merge with defaults.
$instance = wp_parse_args( (array) $instance, $this->defaults );
echo $args['before_widget'];
// Title.
if ( ! empty( $instance['title'] ) ) {
echo $args['before_title'];
echo apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
echo $args['after_title'];
}
// If not being previewed in the Customizer maybe show the dummy preview.
if (
! is_customize_preview() &&
(
false !== strpos( wp_get_referer(), admin_url( 'widgets.php' ) ) ||
false !== strpos( wp_get_referer(), admin_url( 'customize.php' ) )
)
) {
aioseo()->breadcrumbs->frontend->preview();
} else {
aioseo()->breadcrumbs->frontend->display();
}
echo $args['after_widget'];
// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Widget option update.
*
* @since 4.1.1
*
* @param array $newInstance New instance options.
* @param array $oldInstance Old instance options.
* @return array Processed new instance options.
*/
public function update( $newInstance, $oldInstance ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$newInstance['title'] = wp_strip_all_tags( $newInstance['title'] );
return $newInstance;
}
/**
* Widget options form.
*
* @since 4.1.1
*
* @param array $instance The widget instance options.
* @return void
*/
public function form( $instance ) {
// Merge with defaults.
$instance = wp_parse_args( (array) $instance, $this->defaults );
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
<?php echo esc_html( __( 'Title:', 'all-in-one-seo-pack' ) ); ?>
</label>
<input
type="text"
id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
value="<?php echo esc_attr( $instance['title'] ); ?>"
class="widefat"
/>
</p>
<?php
}
} Breadcrumbs/Block.php 0000666 00000012762 15113050716 0010553 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Breadcrumb Block.
*
* @since 4.1.1
*/
class Block {
/**
* The primary term list.
*
* @since 4.3.6
*
* @var array
*/
private $primaryTerm = [];
/**
* The breadcrumb settings.
*
* @since 4.8.3
*
* @var array
*/
private $breadcrumbSettings = [
'default' => true,
'separator' => '›',
'showHomeCrumb' => true,
'showTaxonomyCrumbs' => true,
'showParentCrumbs' => true,
'parentTemplate' => 'default',
'template' => 'default',
'taxonomy' => ''
];
/**
* Class constructor.
*
* @since 4.1.1
*/
public function __construct() {
$this->register();
}
/**
* Registers the block.
*
* @since 4.1.1
*
* @return void
*/
public function register() {
aioseo()->blocks->registerBlock(
'aioseo/breadcrumbs', [
'attributes' => [
'primaryTerm' => [
'type' => 'string',
'default' => null
],
'breadcrumbSettings' => [
'type' => 'object',
'default' => $this->breadcrumbSettings
]
],
'render_callback' => [ $this, 'render' ]
]
);
}
/**
* Renders the block.
*
* @since 4.1.1
*
* @param array $blockAttributes The block attributes.
* @return string The output from the output buffering.
*/
public function render( $blockAttributes ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// phpcs:disable HM.Security.ValidatedSanitizedInput.InputNotSanitized, HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
$postId = ! empty( $_GET['post_id'] ) ? (int) sanitize_text_field( wp_unslash( $_GET['post_id'] ) ) : false;
// phpcs:enable
if ( ! empty( $blockAttributes['primaryTerm'] ) ) {
$this->primaryTerm = json_decode( $blockAttributes['primaryTerm'], true );
}
if ( ! empty( $blockAttributes['breadcrumbSettings'] ) ) {
$this->breadcrumbSettings = $blockAttributes['breadcrumbSettings'];
}
aioseo()->breadcrumbs->setOverride( $this->getBlockOverrides() );
if ( aioseo()->blocks->isRenderingBlockInEditor() && ! empty( $postId ) ) {
add_filter( 'get_object_terms', [ $this, 'temporarilyAddTerm' ], 10, 3 );
$breadcrumbs = aioseo()->breadcrumbs->frontend->sideDisplay( false, 'post' === get_post_type( $postId ) ? 'post' : 'single', get_post( $postId ) );
remove_filter( 'get_object_terms', [ $this, 'temporarilyAddTerm' ], 10 );
if (
in_array( 'breadcrumbsEnable', aioseo()->internalOptions->deprecatedOptions, true ) &&
! aioseo()->options->deprecated->breadcrumbs->enable
) {
return '<p>' .
sprintf(
// Translators: 1 - The plugin short name ("AIOSEO"), 2 - Opening HTML link tag, 3 - Closing HTML link tag.
__( 'Breadcrumbs are currently disabled, so this block will be rendered empty. You can enable %1$s\'s breadcrumb functionality under %2$sGeneral Settings > Breadcrumbs%3$s.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
AIOSEO_PLUGIN_SHORT_NAME,
'<a href="' . esc_url( admin_url( 'admin.php?page=aioseo-settings#/breadcrumbs' ) ) . '" target="_blank">',
'</a>'
) .
'</p>';
}
return $breadcrumbs;
}
return aioseo()->breadcrumbs->frontend->display( false );
}
/**
* Temporarily adds the primary term to the list of terms.
*
* @since 4.3.6
*
* @param array $terms The list of terms.
* @param array $objectIds The object IDs.
* @param array $taxonomies The taxonomies.
* @return array The list of terms.
*/
public function temporarilyAddTerm( $terms, $objectIds, $taxonomies ) {
$taxonomy = $taxonomies[0];
if ( empty( $this->primaryTerm ) || empty( $this->primaryTerm[ $taxonomy ] ) ) {
return $terms;
}
$term = aioseo()->helpers->getTerm( $this->primaryTerm[ $taxonomy ] );
if ( is_a( $term, 'WP_Term' ) ) {
$terms[] = $term;
}
return $terms;
}
/**
* Get the block overrides.
*
* @since 4.8.3
*
* @return array
*/
private function getBlockOverrides() {
$default = filter_var( $this->breadcrumbSettings['default'], FILTER_VALIDATE_BOOLEAN );
if ( true === $default || ! aioseo()->pro ) {
return [];
}
return [
'default' => false,
'taxonomy' => $this->breadcrumbSettings['taxonomy'] ?? '',
'separator' => $this->breadcrumbSettings['separator'] ?? '›',
'showHomeCrumb' => filter_var( $this->breadcrumbSettings['showHomeCrumb'], FILTER_VALIDATE_BOOLEAN ),
'showTaxonomyCrumbs' => filter_var( $this->breadcrumbSettings['showTaxonomyCrumbs'], FILTER_VALIDATE_BOOLEAN ),
'showParentCrumbs' => filter_var( $this->breadcrumbSettings['showParentCrumbs'], FILTER_VALIDATE_BOOLEAN ),
'template' => empty( $this->breadcrumbSettings['template'] ) ? '' : [
'templateType' => 'custom',
'template' => aioseo()->helpers->decodeHtmlEntities( aioseo()->helpers->encodeOutputHtml( $this->breadcrumbSettings['template'] ) )
],
'parentTemplate' => empty( $this->breadcrumbSettings['parentTemplate'] ) ? '' : [
'templateType' => 'custom',
'template' => aioseo()->helpers->decodeHtmlEntities( aioseo()->helpers->encodeOutputHtml( $this->breadcrumbSettings['parentTemplate'] ) )
],
'primaryTerm' => ! empty( $this->primaryTerm[ $this->breadcrumbSettings['taxonomy'] ] ) ? $this->primaryTerm[ $this->breadcrumbSettings['taxonomy'] ] : null
];
}
} Breadcrumbs/Tags.php 0000666 00000026211 15113050716 0010411 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Breadcrumbs;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class to replace tag values with their data counterparts.
*
* @since 4.1.1
*/
class Tags {
/**
* Tags constructor.
*
* @since 4.1.1
*/
public function __construct() {
aioseo()->tags->addContext( $this->getContexts() );
aioseo()->tags->addTags( $this->getTags() );
}
/**
* Replace the tags in the string provided.
*
* @since 4.1.1
*
* @param string $string The string with tags.
* @param array $item The breadcrumb item.
* @param boolean $stripPunctuation Whether we should strip punctuation after the tags have been converted.
* @return string The string with tags replaced.
*/
public function replaceTags( $string, $item, $stripPunctuation = false ) {
if ( ! $string || ! preg_match( '/#/', (string) $string ) ) {
return $string;
}
// Replace separator tag so we don't strip it as punctuation.
$separatorTag = aioseo()->tags->denotationChar . 'separator_sa';
$string = preg_replace( "/$separatorTag(?![a-zA-Z0-9_])/im", '>thisisjustarandomplaceholder<', (string) $string );
// Replace custom breadcrumb tags.
foreach ( $this->getTags() as $tag ) {
$tagId = aioseo()->tags->denotationChar . $tag['id'];
$pattern = "/$tagId(?![a-zA-Z0-9_])/im";
if ( preg_match( $pattern, (string) $string ) ) {
$tagValue = str_replace( '$', '\$', (string) $this->getTagValue( $tag, $item ) );
$string = preg_replace( $pattern, $tagValue, (string) $string );
}
}
if ( $stripPunctuation ) {
$string = aioseo()->helpers->stripPunctuation( $string );
}
// Remove any remaining tags from the title attribute.
$string = preg_replace_callback( '/title="([^"]*)"/i', function ( $matches ) {
$sanitizedTitle = wp_strip_all_tags( html_entity_decode( $matches[1] ) );
return 'title="' . esc_attr( $sanitizedTitle ) . '"';
}, html_entity_decode( $string ) );
return preg_replace(
'/>thisisjustarandomplaceholder<(?![a-zA-Z0-9_])/im',
aioseo()->helpers->decodeHtmlEntities( aioseo()->options->searchAppearance->global->separator ),
(string) $string
);
}
/**
* Get the value of the tag to replace.
*
* @since 4.1.1
*
* @param string $tag The tag to look for.
* @param int $item The crumb array.
* @return string The value of the tag.
*/
public function getTagValue( $tag, $item ) {
$product = false;
if ( 0 === stripos( $tag['id'], 'breadcrumb_wc_product_' ) ) {
$product = wc_get_product( $item['reference'] );
if ( ! $product ) {
return;
}
}
switch ( $tag['id'] ) {
case 'breadcrumb_link':
return $item['link'];
case 'breadcrumb_separator':
return aioseo()->breadcrumbs->frontend->getSeparator();
case 'breadcrumb_wc_product_price':
return $product ? wc_price( $product->get_price() ) : '';
case 'breadcrumb_wc_product_sku':
return $product ? $product->get_sku() : '';
case 'breadcrumb_wc_product_brand':
return $product ? aioseo()->helpers->getWooCommerceBrand( $product->get_id() ) : '';
case 'breadcrumb_author_first_name':
return $item['reference']->first_name;
case 'breadcrumb_author_last_name':
return $item['reference']->last_name;
case 'breadcrumb_archive_post_type_name':
return $item['reference']->label;
case 'breadcrumb_search_string':
return $item['reference'];
case 'breadcrumb_format_page_number':
return $item['reference']['paged'];
default:
return $item['label'];
}
}
/**
* Gets our breadcrumb custom tags.
*
* @since 4.1.1
*
* @return array An array of tags.
*/
public function getTags() {
$tags = [
[
'id' => 'breadcrumb_link',
'name' => __( 'Permalink', 'all-in-one-seo-pack' ),
'description' => __( 'The permalink.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_label',
'name' => __( 'Label', 'all-in-one-seo-pack' ),
'description' => __( 'The label.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_post_title',
// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
'name' => sprintf( __( '%1$s Title', 'all-in-one-seo-pack' ), 'Post' ),
'description' => __( 'The original title of the current post.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_taxonomy_title',
// Translators: 1 - The type of page (Post, Page, Category, Tag, etc.).
'name' => sprintf( __( '%1$s Title', 'all-in-one-seo-pack' ), 'Category' ),
// Translators: 1 - The name of a taxonomy.
'description' => sprintf( __( 'The %1$s title.', 'all-in-one-seo-pack' ), 'Category' )
],
[
'id' => 'breadcrumb_separator',
'name' => __( 'Separator', 'all-in-one-seo-pack' ),
'description' => __( 'The crumb separator.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_blog_page_title',
'name' => __( 'Blog Page Title', 'all-in-one-seo-pack' ),
'description' => __( 'The blog page title.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_author_display_name',
'name' => __( 'Author Display Name', 'all-in-one-seo-pack' ),
'description' => __( 'The author\'s display name.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_author_first_name',
'name' => __( 'Author First Name', 'all-in-one-seo-pack' ),
'description' => __( 'The author\'s first name.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_author_last_name',
'name' => __( 'Author Last Name', 'all-in-one-seo-pack' ),
'description' => __( 'The author\'s last name.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_search_result_format',
'name' => __( 'Search result format', 'all-in-one-seo-pack' ),
'description' => __( 'The search result format.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_404_error_format',
'name' => __( '404 Error Format', 'all-in-one-seo-pack' ),
'description' => __( 'The 404 error format.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_date_archive_year',
'name' => __( 'Year', 'all-in-one-seo-pack' ),
'description' => __( 'The year.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_date_archive_month',
'name' => __( 'Month', 'all-in-one-seo-pack' ),
'description' => __( 'The month.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_date_archive_day',
'name' => __( 'Day', 'all-in-one-seo-pack' ),
'description' => __( 'The day.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_search_string',
'name' => __( 'Search String', 'all-in-one-seo-pack' ),
'description' => __( 'The search string.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_format_page_number',
'name' => __( 'Page Number', 'all-in-one-seo-pack' ),
'description' => __( 'The page number.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_archive_post_type_format',
'name' => __( 'Archive format', 'all-in-one-seo-pack' ),
'description' => __( 'The archive format.', 'all-in-one-seo-pack' )
],
[
'id' => 'breadcrumb_archive_post_type_name',
'name' => __( 'Post Type Name', 'all-in-one-seo-pack' ),
'description' => __( 'The archive post type name.', 'all-in-one-seo-pack' )
]
];
$postTypes = aioseo()->helpers->getPublicPostTypes();
foreach ( $postTypes as $postType ) {
if ( 'product' === $postType['name'] && aioseo()->helpers->isWoocommerceActive() ) {
$tags[] = [
'id' => 'breadcrumb_wc_product_price',
// Translators: 1 - The name of a post type.
'name' => sprintf( __( '%1$s Price', 'all-in-one-seo-pack' ), $postType['singular'] ),
// Translators: 1 - The name of a post type.
'description' => sprintf( __( 'The %1$s price.', 'all-in-one-seo-pack' ), $postType['singular'] )
];
$tags[] = [
'id' => 'breadcrumb_wc_product_sku',
// Translators: 1 - The name of a post type.
'name' => sprintf( __( '%1$s SKU', 'all-in-one-seo-pack' ), $postType['singular'] ),
// Translators: 1 - The name of a post type.
'description' => sprintf( __( 'The %1$s SKU.', 'all-in-one-seo-pack' ), $postType['singular'] )
];
$tags[] = [
'id' => 'breadcrumb_wc_product_brand',
// Translators: 1 - The name of a post type.
'name' => sprintf( __( '%1$s Brand', 'all-in-one-seo-pack' ), $postType['singular'] ),
// Translators: 1 - The name of a post type.
'description' => sprintf( __( 'The %1$s brand.', 'all-in-one-seo-pack' ), $postType['singular'] )
];
}
}
return $tags;
}
/**
* Gets our breadcrumb contexts.
*
* @since 4.1.1
*
* @return array An array of contexts.
*/
public function getContexts() {
$contexts = [];
$baseTags = [ 'breadcrumb_link', 'breadcrumb_separator' ];
$postTypes = aioseo()->helpers->getPublicPostTypes();
foreach ( $postTypes as $postType ) {
$contexts[ 'breadcrumbs-post-type-' . $postType['name'] ] = array_merge( $baseTags, [ 'breadcrumb_post_title' ] );
if ( 'product' === $postType['name'] && aioseo()->helpers->isWoocommerceActive() ) {
$contexts[ 'breadcrumbs-post-type-' . $postType['name'] ] = array_merge( $contexts[ 'breadcrumbs-post-type-' . $postType['name'] ], [
'breadcrumb_wc_product_price',
'breadcrumb_wc_product_sku',
'breadcrumb_wc_product_brand'
] );
}
}
$taxonomies = aioseo()->helpers->getPublicTaxonomies();
foreach ( $taxonomies as $taxonomy ) {
$contexts[ 'breadcrumbs-taxonomy-' . $taxonomy['name'] ] = array_merge( $baseTags, [ 'breadcrumb_taxonomy_title' ] );
}
$archives = aioseo()->helpers->getPublicPostTypes( false, true, true );
foreach ( $archives as $archive ) {
$contexts[ 'breadcrumbs-post-type-archive-' . $archive['name'] ] = array_merge( $baseTags, [
'breadcrumb_archive_post_type_format',
'breadcrumb_archive_post_type_name'
] );
}
$contexts['breadcrumbs-blog-archive'] = array_merge( $baseTags, [ 'breadcrumb_blog_page_title' ] );
$contexts['breadcrumbs-author'] = array_merge( $baseTags, [
'breadcrumb_author_display_name',
'breadcrumb_author_first_name',
'breadcrumb_author_last_name'
] );
$contexts['breadcrumbs-search'] = array_merge( $baseTags, [ 'breadcrumb_search_result_format', 'breadcrumb_search_string' ] );
$contexts['breadcrumbs-notFound'] = array_merge( $baseTags, [ 'breadcrumb_404_error_format' ] );
$contexts['breadcrumbs-date-archive-year'] = array_merge( $baseTags, [ 'breadcrumb_date_archive_year' ] );
$contexts['breadcrumbs-date-archive-month'] = array_merge( $baseTags, [ 'breadcrumb_date_archive_month' ] );
$contexts['breadcrumbs-date-archive-day'] = array_merge( $baseTags, [ 'breadcrumb_date_archive_day' ] );
$contexts['breadcrumbs-format-archive'] = [ 'breadcrumb_archive_post_type_name' ];
$contexts['breadcrumbs-format-search'] = [ 'breadcrumb_search_string' ];
$contexts['breadcrumbs-format-paged'] = [ 'breadcrumb_format_page_number' ];
return $contexts;
}
} Breadcrumbs/Breadcrumbs.php 0000666 00000053445 15113050716 0011755 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Breadcrumbs {
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Breadcrumbs.
*
* @since 4.1.1
*/
class Breadcrumbs {
/** Instance of the frontend class.
*
* @since 4.1.1
*
* @var \AIOSEO\Plugin\Common\Breadcrumbs\Frontend|\AIOSEO\Plugin\Pro\Breadcrumbs\Frontend
*/
public $frontend;
/**
* Instance of the shortcode class.
*
* @since 4.1.1
*
* @var Shortcode
*/
public $shortcode;
/**
* Instance of the block class.
*
* @since 4.1.1
*
* @var Block
*/
public $block;
/**
* Instance of the tags class.
*
* @since 4.1.1
*
* @var Tags
*/
public $tags;
/**
* Array of crumbs.
*
* @since 4.1.1
*
* @var array An array of crumbs.
*/
public $breadcrumbs;
/**
* Array of options to override.
*
* @since 4.8.3
*
* @var array An array of options to override.
*/
protected $override = [];
/**
* Breadcrumbs constructor.
*
* @since 4.1.1
*/
public function __construct() {
$this->frontend = new Frontend();
$this->shortcode = new Shortcode();
$this->block = new Block();
add_action( 'widgets_init', [ $this, 'registerWidget' ] );
// Init Tags class later as we need post types registered.
add_action( 'init', [ $this, 'init' ], 50 );
}
public function init() {
$this->tags = new Tags();
}
/**
* Helper to add crumbs on the breadcrumb array.
*
* @since 4.1.1
*
* @param array $crumbs A single crumb or an array of crumbs.
* @return void
*/
public function addCrumbs( $crumbs ) {
if ( empty( $crumbs ) || ! is_array( $crumbs ) ) {
return;
}
// If it's a single crumb put it inside an array to merge.
if ( isset( $crumbs['label'] ) ) {
$crumbs = [ $crumbs ];
}
$this->breadcrumbs = array_merge( $this->breadcrumbs, $crumbs );
}
/**
* Builds a crumb array based on a type and a reference.
*
* @since 4.1.1
*
* @param string $type The type of breadcrumb ( post, single, page, category, tag, taxonomy, postTypeArchive, date,
* author, search, notFound, blog ).
* @param mixed $reference The reference can be an object ( WP_Post | WP_Term | WP_Post_Type | WP_User ), an array, an int or a string.
* @param array $paged A reference for a paged crumb.
* @return array An array of breadcrumbs with their label, link, type and reference.
*/
public function buildBreadcrumbs( $type, $reference, $paged = [] ) {
// Clear the breadcrumb array and build a new one.
$this->breadcrumbs = [];
// Add breadcrumb prefix.
$this->addCrumbs( $this->getPrefixCrumb( $type, $reference ) );
// Set a home page in the beginning of the breadcrumb.
$this->addCrumbs( $this->maybeGetHomePageCrumb( $type, $reference ) );
// Woocommerce shop page support.
$this->addCrumbs( $this->maybeGetWooCommerceShopCrumb() );
// Blog home.
if (
aioseo()->options->breadcrumbs->showBlogHome &&
in_array( $type, [ 'category', 'tag', 'post', 'author', 'date' ], true )
) {
$this->addCrumbs( $this->getBlogCrumb() );
}
switch ( $type ) {
case 'post':
case 'single':
$this->addCrumbs( $this->getPostArchiveCrumb( $reference ) );
$this->addCrumbs( $this->getPostTaxonomyCrumbs( $reference ) );
$this->addCrumbs( $this->getPostParentCrumbs( $reference ) );
$this->addCrumbs( $this->getPostCrumb( $reference ) );
break;
case 'page':
$this->addCrumbs( $this->getPostParentCrumbs( $reference, 'page' ) );
$this->addCrumbs( $this->getPostCrumb( $reference, 'page' ) );
break;
case 'category':
case 'tag':
case 'taxonomy':
$this->addCrumbs( $this->getTermTaxonomyParentCrumbs( $reference ) );
$this->addCrumbs( $this->getTermTaxonomyCrumb( $reference ) );
break;
case 'postTypeArchive':
$this->addCrumbs( $this->getPostTypeArchiveCrumb( $reference ) );
break;
case 'date':
$this->addCrumbs( $this->getDateCrumb( $reference ) );
break;
case 'author':
$this->addCrumbs( $this->getAuthorCrumb( $reference ) );
break;
case 'blog':
$this->addCrumbs( $this->getBlogCrumb() );
break;
case 'search':
$this->addCrumbs( $this->getSearchCrumb( $reference ) );
break;
case 'notFound':
$this->addCrumbs( $this->getNotFoundCrumb() );
break;
case 'preview':
$this->addCrumbs( $this->getPreviewCrumb( $reference ) );
break;
case 'wcProduct':
$this->addCrumbs( $this->getPostTaxonomyCrumbs( $reference ) );
$this->addCrumbs( $this->getPostParentCrumbs( $reference ) );
$this->addCrumbs( $this->getPostCrumb( $reference ) );
break;
case 'buddypress':
$this->addCrumbs( aioseo()->standalone->buddyPress->component->getCrumbs() );
break;
}
// Paged crumb.
if ( ! empty( $paged['paged'] ) ) {
$this->addCrumbs( $this->getPagedCrumb( $paged ) );
}
// Maybe remove the last crumb.
if ( ! $this->showCurrentItem( $type, $reference ) ) {
array_pop( $this->breadcrumbs );
}
// Remove empty crumbs.
$this->breadcrumbs = array_filter( $this->breadcrumbs );
return $this->breadcrumbs;
}
/**
* Gets the prefix crumb.
*
* @since 4.1.1
*
* @param string $type The type of breadcrumb.
* @param mixed $reference The breadcrumb reference.
* @return array A crumb.
*/
public function getPrefixCrumb( $type, $reference ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( 0 === strlen( aioseo()->options->breadcrumbs->breadcrumbPrefix ) ) {
return [];
}
return $this->makeCrumb( aioseo()->options->breadcrumbs->breadcrumbPrefix, '', 'prefix' );
}
/**
* Gets the 404 crumb.
*
* @since 4.1.1
*
* @return array A crumb.
*/
public function getNotFoundCrumb() {
return $this->makeCrumb( aioseo()->options->breadcrumbs->errorFormat404, '', 'notFound' );
}
/**
* Gets the search crumb.
*
* @since 4.1.1
*
* @param string $searchQuery The search query for reference.
* @return array A crumb.
*/
public function getSearchCrumb( $searchQuery ) {
return $this->makeCrumb( aioseo()->options->breadcrumbs->searchResultFormat, get_search_link( $searchQuery ), 'search', $searchQuery );
}
/**
* Gets the preview crumb.
*
* @since 4.1.5
*
* @param string $label The preview label.
* @return array A crumb.
*/
public function getPreviewCrumb( $label ) {
return $this->makeCrumb( $label, '', 'preview' );
}
/**
* Gets the post type archive crumb.
*
* @since 4.1.1
*
* @param \WP_Post_Type $postType The post type object for reference.
* @return array A crumb.
*/
public function getPostTypeArchiveCrumb( $postType ) {
return $this->makeCrumb( aioseo()->options->breadcrumbs->archiveFormat, get_post_type_archive_link( $postType->name ), 'postTypeArchive', $postType );
}
/**
* Gets a post crumb.
*
* @since 4.1.1
*
* @param \WP_Post $post A post object for reference.
* @param string $type The breadcrumb type.
* @param string $subType The breadcrumb subType.
* @return array A crumb.
*/
public function getPostCrumb( $post, $type = 'single', $subType = '' ) {
return $this->makeCrumb( get_the_title( $post ), get_permalink( $post ), $type, $post, $subType );
}
/**
* Gets the term crumb.
*
* @since 4.1.1
*
* @param \WP_Term $term The term object for reference.
* @param string $subType The breadcrumb subType.
* @return array A crumb.
*/
public function getTermTaxonomyCrumb( $term, $subType = '' ) {
return $this->makeCrumb( $term->name, get_term_link( $term ), 'taxonomy', $term, $subType );
}
/**
* Gets the paged crumb.
*
* @since 4.1.1
*
* @param array $reference The paged array for reference.
* @return array A crumb.
*/
public function getPagedCrumb( $reference ) {
return $this->makeCrumb( sprintf( '%1$s %2$s', __( 'Page', 'all-in-one-seo-pack' ), $reference['paged'] ), $reference['link'], 'paged', $reference );
}
/**
* Gets the author crumb.
*
* @since 4.1.1
*
* @param \WP_User $wpUser A WP_User object.
* @return array A crumb.
*/
public function getAuthorCrumb( $wpUser ) {
return $this->makeCrumb( $wpUser->display_name, get_author_posts_url( $wpUser->ID ), 'author', $wpUser );
}
/**
* Gets the date crumb.
*
* @since 4.1.1
*
* @param array $reference An array of year, month and day values.
* @return array A crumb.
*/
public function getDateCrumb( $reference ) {
$dateCrumb = [];
$addMonth = false;
$addYear = false;
if ( ! empty( $reference['day'] ) ) {
$addMonth = true;
$addYear = true;
$dateCrumb[] = $this->makeCrumb(
zeroise( (int) $reference['day'], 2 ),
get_day_link( $reference['year'], $reference['month'], $reference['day'] ),
'day',
$reference['day']
);
}
if ( ! empty( $reference['month'] ) || $addMonth ) {
$addYear = true;
$dateCrumb[] = $this->makeCrumb(
zeroise( (int) $reference['month'], 2 ),
get_month_link( $reference['year'], $reference['month'] ),
'month',
$reference['month']
);
}
if ( ! empty( $reference['year'] ) || $addYear ) {
$dateCrumb[] = $this->makeCrumb(
$reference['year'],
get_year_link( $reference['year'] ),
'year',
$reference['year']
);
}
return array_reverse( $dateCrumb );
}
/**
* Gets an array of crumbs parents for the term.
*
* @since 4.1.1
*
* @param \WP_Term $term A WP_Term object.
* @return array An array of parent crumbs.
*/
public function getTermTaxonomyParentCrumbs( $term ) {
$crumbs = [];
$termHierarchy = $this->getTermHierarchy( $term->term_id, $term->taxonomy );
if ( ! empty( $termHierarchy ) ) {
foreach ( $termHierarchy as $parentTermId ) {
$parentTerm = aioseo()->helpers->getTerm( $parentTermId, $term->taxonomy );
$crumbs[] = $this->getTermTaxonomyCrumb( $parentTerm, 'parent' );
}
}
return $crumbs;
}
/**
* Helper function to create a standard crumb array.
*
* @since 4.1.1
*
* @param string $label The crumb label.
* @param string $link The crumb url.
* @param null $type The crumb type.
* @param null $reference The crumb reference.
* @param null $subType The crumb subType ( single/parent ).
* @return array A crumb array.
*/
public function makeCrumb( $label, $link = '', $type = null, $reference = null, $subType = null ) {
return [
'label' => $label,
'link' => $link,
'type' => $type,
'subType' => $subType,
'reference' => $reference
];
}
/**
* Gets a post archive crumb if it's post type has archives.
*
* @since 4.1.1
*
* @param int|\WP_Post $post An ID or a WP_Post object.
* @return array A crumb.
*/
public function getPostArchiveCrumb( $post ) {
$postType = get_post_type_object( get_post_type( $post ) );
if ( ! $postType || ! $postType->has_archive ) {
return [];
}
return $this->makeCrumb( $postType->labels->name, get_post_type_archive_link( $postType->name ), 'postTypeArchive', $postType );
}
/**
* Gets a post's taxonomy crumbs.
*
* @since 4.1.1
*
* @param int|\WP_Post $post An ID or a WP_Post object.
* @param null $taxonomy A taxonomy to use. If none is provided the first one with terms selected will be used.
* @return array An array of term crumbs.
*/
public function getPostTaxonomyCrumbs( $post, $taxonomy = null ) {
$crumbs = [];
$overrideTaxonomy = $this->getOverride( 'taxonomy' );
if ( ! empty( $overrideTaxonomy ) ) {
$taxonomy = $overrideTaxonomy;
}
if ( $taxonomy && ! is_array( $taxonomy ) ) {
$taxonomy = [ $taxonomy ];
}
$termHierarchy = $this->getPostTaxTermHierarchy( $post, $taxonomy );
if ( ! empty( $termHierarchy['terms'] ) ) {
foreach ( $termHierarchy['terms'] as $termId ) {
$term = aioseo()->helpers->getTerm( $termId, $termHierarchy['taxonomy'] );
$crumbs[] = $this->makeCrumb( $term->name, get_term_link( $term, $termHierarchy['taxonomy'] ), 'taxonomy', $term, 'parent' );
}
}
return $crumbs;
}
/**
* Gets the post's parent crumbs.
*
* @since 4.1.1
*
* @param int|\WP_Post $post An ID or a WP_Post object.
* @param string $type The crumb type.
* @return array An array of the post parent crumbs.
*/
public function getPostParentCrumbs( $post, $type = 'single' ) {
$crumbs = [];
if ( ! is_post_type_hierarchical( get_post_type( $post ) ) ) {
return $crumbs;
}
$postHierarchy = $this->getPostHierarchy( $post );
if ( ! empty( $postHierarchy ) ) {
foreach ( $postHierarchy as $parentID ) {
// Do not include the Home Page.
if ( aioseo()->helpers->getHomePageId() === $parentID ) {
continue;
}
$crumbs[] = $this->getPostCrumb( get_post( $parentID ), $type, 'parent' );
}
}
return $crumbs;
}
/**
* Function to extend on pro for extra functionality.
*
* @since 4.1.1
*
* @param string $type The type of breadcrumb.
* @param mixed $reference The breadcrumb reference.
* @return bool Show current item.
*/
public function showCurrentItem( $type = null, $reference = null ) {
return apply_filters( 'aioseo_breadcrumbs_show_current_item', aioseo()->options->breadcrumbs->showCurrentItem, $type, $reference );
}
/**
* Gets a home page crumb.
*
* @since 4.1.1
*
* @param string $type The type of breadcrumb.
* @param mixed $reference The breadcrumb reference.
* @return array|void The home crumb.
*/
public function maybeGetHomePageCrumb( $type = null, $reference = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( aioseo()->options->breadcrumbs->homepageLink ) {
return $this->getHomePageCrumb();
}
}
/**
* Gets a home page crumb.
*
* @since 4.1.1
*
* @return array The home crumb.
*/
public function getHomePageCrumb() {
$homePageId = aioseo()->helpers->getHomePageId();
$label = '';
if ( $homePageId ) {
$label = get_the_title( $homePageId );
}
if ( 0 < strlen( aioseo()->options->breadcrumbs->homepageLabel ) ) {
$label = aioseo()->options->breadcrumbs->homepageLabel;
}
// Label fallback.
if ( empty( $label ) ) {
$label = __( 'Home', 'all-in-one-seo-pack' );
}
return $this->makeCrumb( $label, get_home_url(), 'homePage', aioseo()->helpers->getHomePage() );
}
/**
* Gets the blog crumb.
*
* @since 4.1.1
*
* @return array The blog crumb.
*/
public function getBlogCrumb() {
$crumb = [];
$blogPage = aioseo()->helpers->getBlogPage();
if ( null !== $blogPage ) {
$crumb = $this->makeCrumb( $blogPage->post_title, get_permalink( $blogPage ), 'blog', $blogPage );
}
return $crumb;
}
/**
* Maybe add the shop crumb to products and product categories.
*
* @since 4.5.5
*
* @return array The shop crumb.
*/
public function maybeGetWooCommerceShopCrumb() {
$crumb = [];
if (
aioseo()->helpers->isWooCommerceShopPage() ||
aioseo()->helpers->isWooCommerceProductPage() ||
aioseo()->helpers->isWooCommerceTaxonomyPage()
) {
$crumb = $this->getWooCommerceShopCrumb();
}
return $crumb;
}
/**
* Gets the shop crumb.
* @see WC_Breadcrumb::prepend_shop_page()
*
* @since 4.5.5
*
* @return array The shop crumb.
*/
public function getWooCommerceShopCrumb() {
$crumb = [];
if (
! function_exists( 'wc_get_page_id' ) ||
apply_filters( 'aioseo_woocommerce_breadcrumb_hide_shop', false )
) {
return $crumb;
}
$shopPageId = wc_get_page_id( 'shop' );
$shopPage = get_post( $shopPageId );
// WC checks if the permalink contains the shop page in the URI, but we prefer to
// always show the shop page as the first crumb if it exists and it's not the home page.
if (
$shopPageId &&
$shopPage &&
aioseo()->helpers->getHomePageId() !== $shopPageId
) {
$crumb = $this->makeCrumb( get_the_title( $shopPage ), get_permalink( $shopPage ), 'wcShop' );
}
return $crumb;
}
/**
* Gets a post's term hierarchy for a list of taxonomies selecting the one that has a lengthier hierarchy.
*
* @since 4.1.1
*
* @param int|\WP_Post $post An ID or a WP_Post object.
* @param array $taxonomies An array of taxonomy names.
* @param false $skipUnselectedTerms Allow unselected terms to be filtered out from the crumbs.
* @return array An array of the taxonomy name + a term hierarchy.
*/
public function getPostTaxTermHierarchy( $post, $taxonomies = [], $skipUnselectedTerms = false ) {
// Get all taxonomies attached to the post.
if ( empty( $taxonomies ) ) {
$taxonomies = get_object_taxonomies( get_post_type( $post ), 'objects' );
$taxonomies = wp_filter_object_list( $taxonomies, [ 'public' => true ], 'and', 'name' );
}
foreach ( $taxonomies as $taxonomy ) {
$primaryTerm = aioseo()->standalone->primaryTerm->getPrimaryTerm( $post->ID, $taxonomy );
$overridePrimaryTerm = $this->getOverride( 'primaryTerm' );
if ( ! empty( $overridePrimaryTerm ) ) {
$primaryTerm = ! is_a( $overridePrimaryTerm, 'WP_Term' ) ? get_term( $overridePrimaryTerm, $taxonomy ) : $overridePrimaryTerm;
}
$terms = wp_get_object_terms( $post->ID, $taxonomy, [
'orderby' => 'term_id',
'order' => 'ASC',
] );
// Use the first taxonomy with terms.
if ( empty( $terms ) || is_wp_error( $terms ) ) {
continue;
}
// Determines the lengthier term hierarchy.
$termHierarchy = [];
foreach ( $terms as $term ) {
// Gets our filtered ancestors.
$ancestors = $this->getFilteredTermHierarchy( $term->term_id, $term->taxonomy, $skipUnselectedTerms ? $terms : [] );
// Merge the current term to be used in the breadcrumbs.
$ancestors = array_merge( $ancestors, [ $term->term_id ] );
// If the current term is the primary term, use it.
if ( is_a( $primaryTerm, 'WP_Term' ) && $primaryTerm->term_id === $term->term_id ) {
$termHierarchy = $ancestors;
break;
}
$termHierarchy = ( count( $termHierarchy ) < count( $ancestors ) ) ? $ancestors : $termHierarchy;
}
// Return a top to bottom hierarchy.
return [
'taxonomy' => $taxonomy,
'terms' => $termHierarchy
];
}
return [];
}
/**
* Filters a term's parent hierarchy against other terms.
*
* @since 4.1.1
*
* @param int $termId A term id.
* @param string $taxonomy The taxonomy name.
* @param array $termsToFilterAgainst Terms to filter out of the hierarchy.
* @return array The term's parent hierarchy.
*/
public function getFilteredTermHierarchy( $termId, $taxonomy, $termsToFilterAgainst = [] ) {
$ancestors = $this->getTermHierarchy( $termId, $taxonomy );
// Keep only selected terms in the hierarchy.
if ( ! empty( $termsToFilterAgainst ) ) {
// If it's a WP_Term array make it a term_id array.
if ( is_a( current( $termsToFilterAgainst ), 'WP_Term' ) ) {
$termsToFilterAgainst = wp_list_pluck( $termsToFilterAgainst, 'term_id' );
}
$ancestors = array_intersect( $ancestors, $termsToFilterAgainst );
}
return $ancestors;
}
/**
* Gets a term's parent hierarchy.
*
* @since 4.1.1
*
* @param int $termId A term id.
* @param string $taxonomy A taxonomy name.
* @return array The term parent hierarchy.
*/
public function getTermHierarchy( $termId, $taxonomy ) {
// Return a top to bottom hierarchy.
return array_reverse( get_ancestors( $termId, $taxonomy, 'taxonomy' ) );
}
/**
* Gets a post's parent hierarchy.
*
* @since 4.1.1
*
* @param int|\WP_Post $post An ID or a WP_Post object.
* @return array The post parent hierarchy.
*/
public function getPostHierarchy( $post ) {
$postId = ! empty( $post->ID ) ? $post->ID : $post;
// Return a top to bottom hierarchy.
return array_reverse( get_ancestors( $postId, '', 'post_type' ) );
}
/**
* Register our breadcrumb widget.
*
* @since 4.1.1
*
* @return void
*/
public function registerWidget() {
if ( aioseo()->helpers->canRegisterLegacyWidget( 'aioseo-breadcrumb-widget' ) ) {
register_widget( 'AIOSEO\Plugin\Common\Breadcrumbs\Widget' );
}
}
/**
* Setter for the override property.
*
* @since 4.8.3
*
* @param array $toOverride Array containing data to override.
* @return void
*/
public function setOverride( $toOverride = [] ) {
$this->override = $toOverride;
}
/**
* Getter for the override property.
*
* @since 4.8.3
*
* @param string $optionName Optional. The specific option name to retrieve.
* @return array Array containing data to override.
*/
public function getOverride( $optionName = null ) {
if ( empty( $this->override ) ) {
return $optionName ? null : [];
}
$value = $this->override[ $optionName ] ?? null;
return $optionName ? $value : $this->override;
}
}
}
namespace {
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! function_exists( 'aioseo_breadcrumbs' ) ) {
/**
* Global function for breadcrumbs output.
*
* @since 4.1.1
*
* @param boolean $echo Echo or return the output.
* @return string|void The output.
*/
function aioseo_breadcrumbs( $echo = true ) {
return aioseo()->breadcrumbs->frontend->display( $echo );
}
}
} SearchStatistics/KeywordRankTracker.php 0000666 00000020037 15113050716 0014316 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\SearchStatistics;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Keyword Rank Tracker class.
*
* @since 4.7.0
*/
class KeywordRankTracker {
/**
* Retrieves all the keywords' statistics.
*
* @since 4.7.0
*
* @param array $formattedKeywords The formatted keywords.
* @param array $args The arguments.
* @return array The statistics for the keywords.
*/
public function fetchKeywordsStatistics( &$formattedKeywords = [], $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return [
'distribution' => [
'top3' => '6.86',
'top10' => '11.03',
'top50' => '52.10',
'top100' => '30.01',
'difference' => [
'top3' => '24.31',
'top10' => '33.70',
'top50' => '-30.50',
'top100' => '-27.51'
]
],
'distributionIntervals' => [
[
'date' => '2022-10-23',
'top3' => '30.70',
'top10' => '38.60',
'top50' => '24.50',
'top100' => '6.20'
],
[
'date' => '2022-10-30',
'top3' => '31.60',
'top10' => '42.10',
'top50' => '21.00',
'top100' => '5.30'
],
[
'date' => '2022-11-06',
'top3' => '31.30',
'top10' => '44.40',
'top50' => '20.30',
'top100' => '4.00'
],
[
'date' => '2022-11-13',
'top3' => '31.70',
'top10' => '44.20',
'top50' => '20.20',
'top100' => '3.90'
],
[
'date' => '2022-11-20',
'top3' => '31.70',
'top10' => '45.70',
'top50' => '18.00',
'top100' => '4.60'
],
[
'date' => '2022-11-27',
'top3' => '32.50',
'top10' => '47.80',
'top50' => '16.80',
'top100' => '2.90'
],
[
'date' => '2022-12-04',
'top3' => '32.50',
'top10' => '47.20',
'top50' => '17.90',
'top100' => '2.40'
],
[
'date' => '2022-12-11',
'top3' => '31.80',
'top10' => '43.70',
'top50' => '21.00',
'top100' => '3.50'
],
[
'date' => '2022-12-18',
'top3' => '30.40',
'top10' => '43.60',
'top50' => '22.40',
'top100' => '3.60'
],
[
'date' => '2022-12-25',
'top3' => '26.90',
'top10' => '37.20',
'top50' => '29.70',
'top100' => '6.20'
],
[
'date' => '2023-01-01',
'top3' => '27.00',
'top10' => '33.80',
'top50' => '31.60',
'top100' => '7.60'
],
[
'date' => '2023-01-08',
'top3' => '26.60',
'top10' => '38.60',
'top50' => '30.00',
'top100' => '4.80'
],
[
'date' => '2023-01-16',
'top3' => '31.10',
'top10' => '43.90',
'top50' => '22.50',
'top100' => '2.50'
]
]
];
}
/**
* Retrieves all the keywords, formatted.
*
* @since 4.7.0
*
* @param array $args The arguments.
* @return array The formatted keywords.
*/
public function getFormattedKeywords( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$statistics = [];
for ( $i = 1; $i < 9; $i++ ) {
$statistics[ $i ] = [
'clicks' => wp_rand( 1, 1000 ),
'impressions' => wp_rand( 10, 10000 ),
'ctr' => wp_rand( 1, 99 ),
'position' => wp_rand( 1, 100 ),
'history' => [
[
'date' => gmdate( 'Y-m-d', strtotime( '-30 days' ) ),
'position' => wp_rand( 1, 15 ),
'clicks' => wp_rand( 10, 100 ),
],
[
'date' => gmdate( 'Y-m-d', strtotime( '-23 days' ) ),
'position' => wp_rand( 1, 15 ),
'clicks' => wp_rand( 10, 100 ),
],
[
'date' => gmdate( 'Y-m-d', strtotime( '-16 days' ) ),
'position' => wp_rand( 1, 15 ),
'clicks' => wp_rand( 10, 100 ),
],
[
'date' => gmdate( 'Y-m-d', strtotime( '-9 days' ) ),
'position' => wp_rand( 1, 15 ),
'clicks' => wp_rand( 10, 100 ),
],
[
'date' => gmdate( 'Y-m-d', strtotime( '-2 days' ) ),
'position' => wp_rand( 1, 15 ),
'clicks' => wp_rand( 10, 100 ),
]
]
];
}
return [
'rows' => [
[
'id' => 1,
'name' => 'best seo plugin',
'favorited' => false,
'groups' => [
[
'id' => 1,
'name' => 'Blog Pages Group'
]
],
'statistics' => $statistics[1]
],
[
'id' => 2,
'name' => 'aioseo is the best',
'favorited' => true,
'groups' => [
[
'id' => 2,
'name' => 'Low Performance Group'
]
],
'statistics' => $statistics[2]
],
[
'id' => 3,
'name' => 'analyze my seo',
'favorited' => false,
'groups' => [
[
'id' => 3,
'name' => 'High Performance Group'
]
],
'statistics' => $statistics[3]
],
[
'id' => 4,
'name' => 'wordpress seo',
'favorited' => false,
'groups' => [],
'statistics' => $statistics[4]
],
[
'id' => 5,
'name' => 'best seo plugin pro',
'favorited' => false,
'groups' => [],
'statistics' => $statistics[5]
],
[
'id' => 6,
'name' => 'aioseo wordpress',
'favorited' => false,
'groups' => [],
'statistics' => $statistics[6]
],
[
'id' => 7,
'name' => 'headline analyzer aioseo',
'favorited' => false,
'groups' => [],
'statistics' => $statistics[7]
],
[
'id' => 8,
'name' => 'best seo plugin plugin',
'favorited' => false,
'groups' => [],
'statistics' => $statistics[8]
]
],
'totals' => [
'total' => 8,
'pages' => 1,
'page' => 1
],
];
}
/**
* Retrieves all the keyword groups, formatted.
*
* @since 4.7.0
*
* @param array $args The arguments.
* @return array The formatted keyword groups.
*/
public function getFormattedGroups( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$statistics = [];
for ( $i = 1; $i < 4; $i++ ) {
$statistics[ $i ] = [
'clicks' => wp_rand( 1, 1000 ),
'impressions' => wp_rand( 10, 10000 ),
'ctr' => wp_rand( 1, 99 ),
'position' => wp_rand( 1, 100 )
];
}
return [
'rows' => [
[
'id' => 1,
'name' => 'Blog Pages Group',
'keywordsQty' => 1,
'keywords' => [],
'statistics' => $statistics[1]
],
[
'id' => 2,
'name' => 'Low Performance Group',
'keywordsQty' => 1,
'keywords' => [],
'statistics' => $statistics[2]
],
[
'id' => 3,
'name' => 'High Performance Group',
'keywordsQty' => 1,
'keywords' => [],
'statistics' => $statistics[3]
]
],
'totals' => [
'total' => 8,
'pages' => 1,
'page' => 1
],
];
}
/**
* Returns the data for Vue.
*
* @since 4.7.0
*
* @return array The data for Vue.
*/
public function getVueData() {
$formattedKeywords = $this->getFormattedKeywords();
$formattedGroups = $this->getFormattedGroups();
return [
// Dummy data to show on the UI.
'keywords' => [
'all' => $formattedKeywords,
'paginated' => $formattedKeywords,
'count' => count( $formattedKeywords['rows'] ),
'statistics' => $this->fetchKeywordsStatistics( $formattedKeywords ),
],
'groups' => [
'all' => $formattedGroups,
'paginated' => $formattedGroups,
'count' => count( $formattedGroups['rows'] ),
],
];
}
/**
* Returns the data for Vue.
*
* @since 4.7.0
*
* @return array The data.
*/
public function getVueDataEdit() {
$formattedKeywords = $this->getFormattedKeywords();
return [
// Dummy data to show on the UI.
'keywords' => [
'all' => $formattedKeywords,
'paginated' => $formattedKeywords,
'count' => count( $formattedKeywords['rows'] ),
],
];
}
} SearchStatistics/Notices.php 0000666 00000013237 15113050716 0012152 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\SearchStatistics;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Handles the notices for the Search Statistics.
*
* @since 4.6.2
*/
class Notices {
/**
* Class constructor.
*
* @since 4.6.2
*/
public function __construct() {
if ( ! is_admin() ) {
return;
}
add_action( 'init', [ $this, 'init' ] );
}
/**
* Initialize the class.
*
* @since 4.6.2
*
* @return void
*/
public function init() {
$this->siteConnected();
$this->siteVerified();
$this->sitemapHasErrors();
}
/**
* Add a notice if the site is not connected.
*
* @since 4.6.2
*
* @return void
*/
private function siteConnected() {
$notification = Models\Notification::getNotificationByName( 'search-console-site-not-connected' );
if ( aioseo()->searchStatistics->api->auth->isConnected() ) {
if ( $notification->exists() ) {
Models\Notification::deleteNotificationByName( 'search-console-site-not-connected' );
}
return;
}
if ( $notification->exists() ) {
return;
}
Models\Notification::addNotification( [
'slug' => uniqid(),
'notification_name' => 'search-console-site-not-connected',
'title' => __( 'Have you connected your site to Google Search Console?', 'all-in-one-seo-pack' ),
'content' => sprintf(
// Translators: 1 - The plugin short name ("AIOSEO").
__( '%1$s can now verify whether your site is correctly verified with Google Search Console and that your sitemaps have been submitted correctly. Connect with Google Search Console now to ensure your content is being added to Google as soon as possible for increased rankings.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
AIOSEO_PLUGIN_SHORT_NAME
),
'type' => 'warning',
'level' => [ 'all' ],
'button1_label' => __( 'Connect to Google Search Console', 'all-in-one-seo-pack' ),
'button1_action' => 'https://route#aioseo-settings&aioseo-scroll=google-search-console-settings&aioseo-highlight=google-search-console-settings:webmaster-tools?activetool=googleSearchConsole', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
'start' => gmdate( 'Y-m-d H:i:s' )
] );
}
/**
* Add a notice if the site is not verified or was deleted.
*
* @since 4.6.2
*
* @return void
*/
private function siteVerified() {
$notification = Models\Notification::getNotificationByName( 'search-console-site-not-verified' );
if (
! aioseo()->searchStatistics->api->auth->isConnected() ||
aioseo()->internalOptions->searchStatistics->site->verified ||
0 === aioseo()->internalOptions->searchStatistics->site->lastFetch // Not fetched yet.
) {
if ( $notification->exists() ) {
Models\Notification::deleteNotificationByName( 'search-console-site-not-verified' );
}
return;
}
if ( $notification->exists() ) {
return;
}
Models\Notification::addNotification( [
'slug' => uniqid(),
'notification_name' => 'search-console-site-not-verified',
'title' => __( 'Your site was removed from Google Search Console.', 'all-in-one-seo-pack' ),
'content' => __( 'We detected that your site has been removed from Google Search Console. If this was done in error, click below to re-sync and resolve this issue.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
'type' => 'warning',
'level' => [ 'all' ],
'button1_label' => __( 'Reconnect Google Search Console', 'all-in-one-seo-pack' ),
'button1_action' => 'https://route#aioseo-settings&aioseo-scroll=google-search-console-settings&aioseo-highlight=google-search-console-settings:webmaster-tools?activetool=googleSearchConsole', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
'start' => gmdate( 'Y-m-d H:i:s' )
] );
}
/**
* Add a notice if the sitemap has errors.
*
* @since 4.6.2
*
* @return void
*/
private function sitemapHasErrors() {
$notification = Models\Notification::getNotificationByName( 'search-console-sitemap-has-errors' );
if (
! aioseo()->searchStatistics->api->auth->isConnected() ||
! aioseo()->internalOptions->searchStatistics->site->verified ||
0 === aioseo()->internalOptions->searchStatistics->sitemap->lastFetch || // Not fetched yet.
! aioseo()->searchStatistics->sitemap->getSitemapsWithErrors()
) {
if ( $notification->exists() ) {
Models\Notification::deleteNotificationByName( 'search-console-sitemap-has-errors' );
}
return;
}
if ( $notification->exists() ) {
return;
}
$lastFetch = aioseo()->internalOptions->searchStatistics->sitemap->lastFetch;
$lastFetch = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $lastFetch );
Models\Notification::addNotification( [
'slug' => uniqid(),
'notification_name' => 'search-console-sitemap-has-errors',
'title' => __( 'Your sitemap has errors.', 'all-in-one-seo-pack' ),
'content' => sprintf(
// Translators: 1 - Last fetch date.
__( 'We detected that your sitemap has errors. The last fetch was on %1$s. Click below to resolve this issue.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
$lastFetch
),
'type' => 'warning',
'level' => [ 'all' ],
'button1_label' => __( 'Fix Sitemap Errors', 'all-in-one-seo-pack' ),
'button1_action' => 'https://route#aioseo-sitemaps&open-modal=true:general-sitemap', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
'start' => gmdate( 'Y-m-d H:i:s' )
] );
}
} SearchStatistics/Site.php 0000666 00000007077 15113050716 0011457 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\SearchStatistics;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the site for the search statistics.
*
* @since 4.6.2
*/
class Site {
/**
* The action name.
*
* @since 4.6.2
*
* @var string
*/
public $action = 'aioseo_search_statistics_site_check';
/**
* Class constructor.
*
* @since 4.6.2
*/
public function __construct() {
add_action( 'admin_init', [ $this, 'init' ] );
add_action( $this->action, [ $this, 'worker' ] );
}
/**
* Initialize the class.
*
* @since 4.6.2
*
* @return void
*/
public function init() {
if (
! aioseo()->searchStatistics->api->auth->isConnected() ||
aioseo()->actionScheduler->isScheduled( $this->action )
) {
return;
}
aioseo()->actionScheduler->scheduleAsync( $this->action );
}
/**
* Check whether the site is verified on Google Search Console and verifies it if needed.
*
* @since 4.6.2
*
* @return void
*/
public function worker() {
if ( ! aioseo()->searchStatistics->api->auth->isConnected() ) {
return;
}
$siteStatus = $this->checkStatus();
if ( empty( $siteStatus ) ) {
// If it failed to communicate with the server, try again in a few hours.
aioseo()->actionScheduler->scheduleSingle( $this->action, wp_rand( HOUR_IN_SECONDS, 2 * HOUR_IN_SECONDS ), [], true );
return;
}
$this->processStatus( $siteStatus );
// Schedule a new check for the next week.
aioseo()->actionScheduler->scheduleSingle( $this->action, WEEK_IN_SECONDS + wp_rand( 0, 3 * DAY_IN_SECONDS ), [], true );
}
/**
* Maybe verifies the site on Google Search Console.
*
* @since 4.6.2
*
* @return void
*/
public function maybeVerify() {
if ( ! aioseo()->searchStatistics->api->auth->isConnected() ) {
return;
}
$siteStatus = $this->checkStatus();
if ( empty( $siteStatus ) ) {
return;
}
$this->processStatus( $siteStatus );
}
/**
* Checks the site status on Google Search Console.
*
* @since 4.6.2
*
* @return array The site status.
*/
private function checkStatus() {
$api = new Api\Request( 'google-search-console/site/check/' );
$response = $api->request();
if ( is_wp_error( $response ) ) {
return [];
}
return $response;
}
/**
* Processes the site status.
*
* @since 4.6.3
*
* @param array $siteStatus The site status.
* @return void
*/
private function processStatus( $siteStatus ) {
switch ( $siteStatus['code'] ) {
case 'site_verified':
aioseo()->internalOptions->searchStatistics->site->verified = true;
aioseo()->internalOptions->searchStatistics->site->lastFetch = time();
break;
case 'verification_needed':
$this->verify( $siteStatus['data'] );
break;
case 'site_not_found':
case 'couldnt_get_token':
default:
aioseo()->internalOptions->searchStatistics->site->verified = false;
aioseo()->internalOptions->searchStatistics->site->lastFetch = time();
}
}
/**
* Verifies the site on Google Search Console.
*
* @since 4.6.2
*
* @param string $token The verification token.
* @return void
*/
private function verify( $token = '' ) {
if ( empty( $token ) ) {
return;
}
aioseo()->options->webmasterTools->google = esc_attr( $token );
$api = new Api\Request( 'google-search-console/site/verify/' );
$response = $api->request();
if ( is_wp_error( $response ) || 'site_verified' !== $response['code'] ) {
return;
}
aioseo()->internalOptions->searchStatistics->site->verified = true;
aioseo()->internalOptions->searchStatistics->site->lastFetch = time();
}
} SearchStatistics/IndexStatus.php 0000666 00000023210 15113050716 0013011 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\SearchStatistics;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Index Status class.
*
* @since 4.8.2
*/
class IndexStatus {
/**
* Retrieves the overview data.
*
* @since 4.8.2
*
* @return array The overview data.
*/
public function getOverview() {
$data = [
'post' => [
'results' => [
[
'count' => 164,
'coverageState' => 'Submitted and Indexed', // No need to translate this. It's translated on the front-end.
],
[
'count' => 112,
'coverageState' => 'Discovered - Currently Not Indexed',
],
[
'count' => 44,
'coverageState' => 'Crawled - Currently Not Indexed',
],
[
'count' => 8,
'coverageState' => 'URL is unknown to Google',
]
]
]
];
$data['post']['total'] = array_sum( array_column( $data['post']['results'], 'count' ) );
return $data;
}
/**
* Retrieves all the objects, formatted.
*
* @since 4.8.2
*
* @return array The formatted objects.
*/
public function getFormattedObjects() {
$siteUrl = aioseo()->helpers->getSiteUrl();
$rows = [
[
'objectId' => 4,
'objectTitle' => 'Homepage',
'verdict' => 'PASS',
'coverageState' => 'Submitted and Indexed',
'robotsTxtState' => 'ALLOWED',
'indexingState' => 'INDEXING_ALLOWED',
'pageFetchState' => 'SUCCESSFUL',
'crawledAs' => 'MOBILE',
'lastCrawlTime' => aioseo()->helpers->dateToWpFormat( '2025-01-05 13:54:00' ),
'userCanonical' => $siteUrl,
'googleCanonical' => $siteUrl,
'sitemap' => [
aioseo()->sitemap->helpers->getUrl( 'general' )
],
'referringUrls' => [],
'richResultsResult' => [
'detectedItems' => [
[
'richResultType' => 'Breadcrumbs',
'items' => [
[
'name' => 'Unnamed item'
]
]
],
[
'richResultType' => 'FAQ',
'items' => [
[
'name' => 'Unnamed item'
]
]
]
]
],
'inspectionResultLink' => '#',
'richResultsTestLink' => '#'
],
[
'objectId' => 6,
'objectTitle' => 'About',
'verdict' => 'PASS',
'coverageState' => 'Submitted and Indexed',
'robotsTxtState' => 'ALLOWED',
'indexingState' => 'INDEXING_ALLOWED',
'pageFetchState' => 'SUCCESSFUL',
'crawledAs' => 'MOBILE',
'lastCrawlTime' => aioseo()->helpers->dateToWpFormat( '2025-01-06 09:22:00' ),
'userCanonical' => $siteUrl . '/about',
'googleCanonical' => $siteUrl . '/about',
'sitemap' => [
aioseo()->sitemap->helpers->getUrl( 'general' )
],
'referringUrls' => [
$siteUrl
],
'richResultsResult' => [
'detectedItems' => [
[
'richResultType' => 'Breadcrumbs',
'items' => [
[
'name' => 'Unnamed item'
]
]
]
]
],
'inspectionResultLink' => '#',
'richResultsTestLink' => '#'
],
[
'objectId' => 1,
'objectTitle' => 'Contact Us',
'verdict' => 'PASS',
'coverageState' => 'Submitted and Indexed',
'robotsTxtState' => 'ALLOWED',
'indexingState' => 'INDEXING_ALLOWED',
'pageFetchState' => 'SUCCESSFUL',
'crawledAs' => 'DESKTOP',
'lastCrawlTime' => aioseo()->helpers->dateToWpFormat( '2025-01-02 16:47:00' ),
'userCanonical' => $siteUrl . '/contact-us',
'googleCanonical' => $siteUrl . '/contact-us',
'sitemap' => [
aioseo()->sitemap->helpers->getUrl( 'general' )
],
'referringUrls' => [
$siteUrl
],
'richResultsResult' => [
'detectedItems' => [
[
'richResultType' => 'Breadcrumbs',
'items' => [
[
'name' => 'Unnamed item'
]
]
],
[
'richResultType' => 'FAQ',
'items' => [
[
'name' => 'Unnamed item'
]
]
]
]
],
'inspectionResultLink' => '#',
'richResultsTestLink' => '#'
],
[
'objectId' => 2,
'objectTitle' => 'Pricing',
'verdict' => 'NEUTRAL',
'coverageState' => 'Crawled - Currently Not Indexed',
'robotsTxtState' => 'DISALLOWED',
'indexingState' => 'BLOCKED_BY_META_TAG',
'pageFetchState' => 'SUCCESSFUL',
'crawledAs' => 'DESKTOP',
'lastCrawlTime' => aioseo()->helpers->dateToWpFormat( '2024-01-15 11:00:00' ),
'userCanonical' => $siteUrl . '/pricing',
'googleCanonical' => $siteUrl . '/pricing',
'sitemap' => [
aioseo()->sitemap->helpers->getUrl( 'general' )
],
'referringUrls' => [
$siteUrl
],
'richResultsResult' => [
'detectedItems' => [
[
'richResultType' => 'Breadcrumbs',
'items' => [
[
'name' => 'Unnamed item'
]
]
],
[
'richResultType' => 'Product snippet',
'items' => [
[
'name' => 'All in One SEO (AIOSEO)',
'issues' => [
[
'issueMessage' => 'Missing field "priceValidUntil"',
'severity' => 'WARNING'
]
]
]
]
]
]
],
'inspectionResultLink' => '#',
'richResultsTestLink' => '#'
],
[
'objectId' => 3,
'objectTitle' => 'Blog',
'verdict' => 'PASS',
'coverageState' => 'Submitted and Indexed',
'robotsTxtState' => 'ALLOWED',
'indexingState' => 'INDEXED',
'pageFetchState' => 'SUCCESSFUL',
'crawledAs' => 'MOBILE',
'lastCrawlTime' => aioseo()->helpers->dateToWpFormat( '2024-03-01 08:00:00' ),
'userCanonical' => $siteUrl . '/blog',
'googleCanonical' => $siteUrl . '/blog',
'sitemap' => [
aioseo()->sitemap->helpers->getUrl( 'general' )
],
'referringUrls' => [
$siteUrl
],
'inspectionResultLink' => '#',
'richResultsTestLink' => '#'
],
];
return [
'paginated' => [
'rows' => $rows,
'totals' => [
'total' => count( $rows ),
'pages' => 1,
'page' => 1
]
]
];
}
/**
* Returns the data for Vue.
*
* @since 4.8.2
*
* @return array The data for Vue.
*/
public function getVueData() {
return [
'objects' => $this->getFormattedObjects(),
'overview' => $this->getOverview(),
'options' => $this->getUiOptions()
];
}
/**
* Retrieves options ideally only for Vue usage on the front-end.
*
* @since 4.8.2
*
* @return array The options.
*/
protected function getUiOptions() {
$postTypeOptions = [
[
'label' => __( 'All Post Types', 'all-in-one-seo-pack' ),
'value' => ''
],
[
'label' => __( 'Post', 'all-in-one-seo-pack' ),
'value' => 'post'
],
[
'label' => __( 'Page', 'all-in-one-seo-pack' ),
'value' => 'page'
]
];
$statusOptions = [
[
'label' => __( 'Status (All)', 'all-in-one-seo-pack' ),
'value' => ''
],
[
'label' => __( 'Indexed', 'all-in-one-seo-pack' ),
'value' => 'submitted',
'color' => '#00AA63',
],
[
'label' => __( 'Crawled, Not Indexed', 'all-in-one-seo-pack' ),
'value' => 'crawled',
'color' => '#F18200',
],
[
'label' => __( 'Discovered, Not Indexed', 'all-in-one-seo-pack' ),
'value' => 'discovered',
'color' => '#005AE0',
],
[
'label' => __( 'Other, Not Indexed', 'all-in-one-seo-pack' ),
'value' => 'unknown|excluded|invalid|error',
'color' => '#DF2A4A',
],
[
'label' => __( 'No Results Yet', 'all-in-one-seo-pack' ),
'value' => 'empty',
'color' => '#999999',
]
];
$robotsTxtStateOptions = [
[
'label' => __( 'Robots.txt (All)', 'all-in-one-seo-pack' ),
'value' => ''
],
[
'label' => __( 'Allowed', 'all-in-one-seo-pack' ),
'value' => 'ALLOWED'
],
[
'label' => __( 'Blocked', 'all-in-one-seo-pack' ),
'value' => 'DISALLOWED'
]
];
$crawledAsOptions = [
[
'label' => __( 'Crawled As (All)', 'all-in-one-seo-pack' ),
'value' => ''
],
[
'label' => __( 'Desktop', 'all-in-one-seo-pack' ),
'value' => 'DESKTOP'
],
[
'label' => __( 'Mobile', 'all-in-one-seo-pack' ),
'value' => 'MOBILE'
]
];
$pageFetchStateOptions = [
[
'label' => __( 'Page Fetch (All)', 'all-in-one-seo-pack' ),
'value' => ''
],
[
'label' => __( 'Successful', 'all-in-one-seo-pack' ),
'value' => 'SUCCESSFUL'
],
[
'label' => __( 'Error', 'all-in-one-seo-pack' ),
'value' => 'SOFT_404,BLOCKED_ROBOTS_TXT,NOT_FOUND,ACCESS_DENIED,SERVER_ERROR,REDIRECT_ERROR,ACCESS_FORBIDDEN,BLOCKED_4XX,INTERNAL_CRAWL_ERROR,INVALID_URL'
]
];
$additionalFilters = [
'postTypeOptions' => [
'name' => 'postType',
'options' => $postTypeOptions
],
'statusOptions' => [
'name' => 'status',
'options' => $statusOptions
],
'robotsTxtStateOptions' => [
'name' => 'robotsTxtState',
'options' => $robotsTxtStateOptions
],
'pageFetchStateOptions' => [
'name' => 'pageFetchState',
'options' => $pageFetchStateOptions
],
'crawledAsOptions' => [
'name' => 'crawledAs',
'options' => $crawledAsOptions
],
];
return [
'table' => [
'additionalFilters' => $additionalFilters
]
];
}
} SearchStatistics/Sitemap.php 0000666 00000007546 15113050716 0012156 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\SearchStatistics;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the sitemaps for the search statistics.
*
* @since 4.6.2
*/
class Sitemap {
/**
* The action name.
*
* @since 4.6.2
*
* @var string
*/
public $action = 'aioseo_search_statistics_sitemap_sync';
/**
* Class constructor.
*
* @since 4.6.2
*/
public function __construct() {
add_action( 'admin_init', [ $this, 'init' ] );
add_action( $this->action, [ $this, 'worker' ] );
}
/**
* Initialize the class.
*
* @since 4.6.2
*
* @return void
*/
public function init() {
if (
! aioseo()->searchStatistics->api->auth->isConnected() ||
! aioseo()->internalOptions->searchStatistics->site->verified ||
aioseo()->actionScheduler->isScheduled( $this->action )
) {
return;
}
aioseo()->actionScheduler->scheduleAsync( $this->action );
}
/**
* Sync the sitemap.
*
* @since 4.6.3
*
* @return void
*/
public function worker() {
if ( ! $this->canSync() ) {
return;
}
$api = new Api\Request( 'google-search-console/sitemap/sync/', [ 'sitemaps' => aioseo()->sitemap->helpers->getSitemapUrls() ] );
$response = $api->request();
if ( is_wp_error( $response ) || empty( $response['data'] ) ) {
// If it failed to communicate with the server, try again in a few hours.
aioseo()->actionScheduler->scheduleSingle( $this->action, wp_rand( HOUR_IN_SECONDS, 2 * HOUR_IN_SECONDS ), [], true );
return;
}
aioseo()->internalOptions->searchStatistics->sitemap->list = $response['data'];
aioseo()->internalOptions->searchStatistics->sitemap->lastFetch = time();
// Schedule a new sync for the next week.
aioseo()->actionScheduler->scheduleSingle( $this->action, WEEK_IN_SECONDS + wp_rand( 0, 3 * DAY_IN_SECONDS ), [], true );
}
/**
* Maybe sync the sitemap after updating the options.
* It will check whether the sitemap options have changed and sync the sitemap if needed.
*
* @since 4.6.2
*
* @param array $oldSitemapOptions The old sitemap options.
* @param array $newSitemapOptions The new sitemap options.
*
* @return void
*/
public function maybeSync( $oldSitemapOptions, $newSitemapOptions ) {
if (
! $this->canSync() ||
empty( $oldSitemapOptions ) ||
empty( $newSitemapOptions )
) {
return;
}
// Ignore the HTML sitemap, since it's not actually a sitemap to be synced with Google.
unset( $newSitemapOptions['html'] );
$shouldResync = false;
foreach ( $newSitemapOptions as $type => $options ) {
if ( empty( $oldSitemapOptions[ $type ] ) ) {
continue;
}
if ( $oldSitemapOptions[ $type ]['enable'] !== $options['enable'] ) {
$shouldResync = true;
break;
}
}
if ( ! $shouldResync ) {
return;
}
aioseo()->actionScheduler->unschedule( $this->action );
aioseo()->actionScheduler->scheduleAsync( $this->action );
}
/**
* Get the sitemaps with errors.
*
* @since 4.6.2
*
* @return array
*/
public function getSitemapsWithErrors() {
$sitemaps = aioseo()->internalOptions->searchStatistics->sitemap->list;
$ignored = aioseo()->internalOptions->searchStatistics->sitemap->ignored;
if ( empty( $sitemaps ) ) {
return [];
}
$errors = [];
$pluginSitemaps = aioseo()->sitemap->helpers->getSitemapUrls();
foreach ( $sitemaps as $sitemap ) {
if (
empty( $sitemap['errors'] ) ||
in_array( $sitemap['path'], $ignored, true ) || // Skip user-ignored sitemaps.
in_array( $sitemap['path'], $pluginSitemaps, true ) // Skip plugin sitemaps.
) {
continue;
}
$errors[] = $sitemap;
}
return $errors;
}
/**
* Check if the sitemap can be synced.
*
* @since 4.6.2
*
* @return bool
*/
private function canSync() {
return aioseo()->searchStatistics->api->auth->isConnected() && aioseo()->internalOptions->searchStatistics->site->verified;
}
} SearchStatistics/SearchStatistics.php 0000666 00000075513 15113050717 0014034 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\SearchStatistics;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class that holds our Search Statistics feature.
*
* @since 4.3.0
*/
class SearchStatistics {
/**
* Holds the instance of the API class.
*
* @since 4.3.0
*
* @var Api\Api
*/
public $api;
/**
* Holds the instance of the Site class.
*
* @since 4.6.2
*
* @var Site
*/
public $site;
/**
* Holds the instance of the Sitemap class.
*
* @since 4.6.2
*
* @var Sitemap
*/
public $sitemap;
/**
* Holds the instance of the Notices class.
*
* @since 4.6.2
*
* @var Notices
*/
public $notices;
/**
* Holds the instance of the Keyword Rank Tracker class.
*
* @since 4.7.0
*
* @var KeywordRankTracker
*/
public $keywordRankTracker;
/**
* Holds the instance of the Index Status class.
*
* @since 4.8.2
*
* @var IndexStatus
*/
public $indexStatus;
/**
* Class constructor.
*
* @since 4.3.0
*/
public function __construct() {
$this->api = new Api\Api();
$this->site = new Site();
$this->sitemap = new Sitemap();
$this->notices = new Notices();
$this->keywordRankTracker = new KeywordRankTracker();
$this->indexStatus = new IndexStatus();
}
/**
* Returns the data for Vue.
*
* @since 4.3.0
*
* @return array The data for Vue.
*/
public function getVueData() {
$data = [
'isConnected' => aioseo()->searchStatistics->api->auth->isConnected(),
'latestAvailableDate' => null,
'range' => [],
'rolling' => aioseo()->internalOptions->internal->searchStatistics->rolling,
'authedSite' => aioseo()->searchStatistics->api->auth->getAuthedSite(),
'sitemapsWithErrors' => aioseo()->searchStatistics->sitemap->getSitemapsWithErrors(),
'data' => [
'seoStatistics' => $this->getSeoOverviewData(),
'keywords' => $this->getKeywordsData(),
'contentRankings' => $this->getContentRankingsData()
]
];
return $data;
}
/**
* Resets the Search Statistics.
*
* @since 4.6.2
*
* @return void
*/
public function reset() {
aioseo()->internalOptions->searchStatistics->sitemap->reset();
aioseo()->internalOptions->searchStatistics->site->reset();
// Clear the cache for the Search Statistics.
aioseo()->searchStatistics->clearCache();
}
/**
* Returns the data for the SEO Overview.
*
* @since 4.3.0
*
* @param array $dateRange The date range.
* @return array The data for the SEO Overview.
*/
protected function getSeoOverviewData( $dateRange = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$pageRows = [
'/' => [
'ctr' => '1.25',
'page' => '/',
'clicks' => 15460,
'position' => '74.01',
'difference' => [
'ctr' => '-0.23',
'decay' => 192211,
'clicks' => -26,
'current' => true,
'position' => '19.66',
'comparison' => true,
'impressions' => 192237
],
'impressions' => 1235435,
'context' => [],
'objectId' => 0,
'objectTitle' => '/',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'seoScore' => 65
],
'/test-page/' => [
'ctr' => '0.30',
'page' => '/test-page/',
'clicks' => 5688,
'position' => '35.28',
'difference' => [
'ctr' => '0.05',
'decay' => 378973,
'clicks' => 1941,
'current' => true,
'position' => '-2.33',
'comparison' => true,
'impressions' => 377032
],
'impressions' => 1881338,
'context' => [],
'objectId' => 0,
'objectTitle' => '/test-page/',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'seoScore' => 95
],
'/high-ranking-page/' => [
'ctr' => '6.03',
'page' => '/high-ranking-page/',
'clicks' => 3452,
'position' => '22.85',
'difference' => [
'ctr' => '-0.95',
'decay' => -5986,
'clicks' => -898,
'current' => true,
'position' => '-0.22',
'comparison' => true,
'impressions' => -5088
],
'impressions' => 57248,
'context' => [],
'objectId' => 0,
'objectTitle' => '/high-ranking-page/',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'seoScore' => 100
],
'/pricing/' => [
'ctr' => '1.35',
'page' => '/pricing/',
'clicks' => 2749,
'position' => '40.40',
'difference' => [
'ctr' => '-0.16',
'decay' => 15991,
'clicks' => -94,
'current' => true,
'position' => '9.77',
'comparison' => true,
'impressions' => 16085
],
'impressions' => 203794,
'context' => [],
'objectId' => 0,
'objectTitle' => '/pricing/',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'seoScore' => 100
],
'/features-and-benefits/' => [
'ctr' => '2.48',
'page' => '/features-and-benefits/',
'clicks' => 2600,
'position' => '15.53',
'difference' => [
'ctr' => '0.99',
'decay' => 23466,
'clicks' => 1367,
'current' => true,
'position' => '1.51',
'comparison' => true,
'impressions' => 22099
],
'impressions' => 104769,
'context' => [],
'objectId' => 0,
'objectTitle' => '/features-and-benefits/',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'seoScore' => 90
],
'/documentation/' => [
'ctr' => '2.64',
'page' => '/documentation/',
'clicks' => 1716,
'position' => '27.85',
'difference' => [
'ctr' => '-0.04',
'decay' => 7274,
'clicks' => 167,
'current' => true,
'position' => '-9.51',
'comparison' => true,
'impressions' => 7107
],
'impressions' => 64883,
'context' => [],
'objectId' => 0,
'objectTitle' => '/documentation/',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'seoScore' => 93
],
'/blog/' => [
'ctr' => '3.75',
'page' => '/blog/',
'clicks' => 1661,
'position' => '36.60',
'difference' => [
'ctr' => '0.42',
'decay' => -3145,
'clicks' => 77,
'current' => true,
'position' => '-9.40',
'comparison' => true,
'impressions' => -3222
],
'impressions' => 44296,
'context' => [],
'objectId' => 0,
'objectTitle' => '/blog/',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'seoScore' => 97
],
'/blog/my-best-content/' => [
'ctr' => '7.08',
'page' => '/blog/my-best-content/',
'clicks' => 1573,
'position' => '9.61',
'difference' => [
'ctr' => '0.16',
'decay' => -201,
'clicks' => 22,
'current' => true,
'position' => '-2.03',
'comparison' => true,
'impressions' => -223
],
'impressions' => 22203,
'context' => [],
'objectId' => 0,
'objectTitle' => '/blog/my-best-content/',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'seoScore' => 56
],
'/contact-us/' => [
'ctr' => '1.45',
'page' => '/contact-us/',
'clicks' => 1550,
'position' => '32.05',
'difference' => [
'ctr' => '0.12',
'decay' => 1079,
'clicks' => 140,
'current' => true,
'position' => '-3.47',
'comparison' => true,
'impressions' => 939
],
'impressions' => 106921,
'context' => [],
'objectId' => 0,
'objectTitle' => '/contact-us/',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'seoScore' => 78
],
'/support/' => [
'ctr' => '5.94',
'page' => '/support/',
'clicks' => 1549,
'position' => '25.83',
'difference' => [
'ctr' => '-0.74',
'decay' => 3885,
'clicks' => 62,
'current' => true,
'position' => '-1.48',
'comparison' => true,
'impressions' => 3823
],
'impressions' => 26099,
'context' => [],
'objectId' => 0,
'objectTitle' => '/support/',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'seoScore' => 86
]
];
// Get the 10 most recent posts.
$recentPosts = aioseo()->db->db->get_results(
sprintf(
'SELECT ID, post_title FROM %1$s WHERE post_status = "publish" AND post_type = "post" ORDER BY post_date DESC LIMIT 10',
aioseo()->db->db->posts
)
);
// Loop through the default page rows and update the key with the permalink from the most recent posts.
$i = 0;
foreach ( $pageRows as $key => $pageRow ) {
// Get the permalink of the recent post that matches the $i index.
$permalink = isset( $recentPosts[ $i ] ) ? get_permalink( $recentPosts[ $i ]->ID ) : '';
// If we don't have a permalink, continue to the next row.
if ( empty( $permalink ) ) {
continue;
}
// Remove the domain from the permalink by parsing the URL and getting the path.
$permalink = wp_parse_url( $permalink, PHP_URL_PATH );
// If the permalink already exists, continue to the next row.
if ( isset( $pageRows[ $permalink ] ) ) {
// Update the objectId and objectTitle with the recent post ID and title.
$pageRows[ $permalink ]['objectId'] = $recentPosts[ $i ]->ID;
$pageRows[ $permalink ]['objectTitle'] = $recentPosts[ $i ]->post_title;
continue;
}
$pageRows[ $permalink ] = $pageRows[ $key ];
// Remove the old key.
unset( $pageRows[ $key ] );
// Update the objectId and objectTitle with the recent post ID and title.
$pageRows[ $permalink ]['objectId'] = $recentPosts[ $i ]->ID;
$pageRows[ $permalink ]['objectTitle'] = $recentPosts[ $i ]->post_title;
$i++;
}
return [
'statistics' => [
'ctr' => '0.74',
'clicks' => 111521,
'keywords' => 19335,
'position' => '49.28',
'difference' => [
'ctr' => '0.03',
'clicks' => 1736,
'keywords' => 2853,
'position' => '1.01',
'impressions' => -475679
],
'impressions' => 14978074
],
'intervals' => [
[
'ctr' => '0.72',
'date' => '2022-10-23',
'clicks' => 7091,
'position' => '48.88',
'impressions' => 985061
],
[
'ctr' => '0.77',
'date' => '2022-10-30',
'clicks' => 8544,
'position' => '46.48',
'impressions' => 1111602
],
[
'ctr' => '0.73',
'date' => '2022-11-06',
'clicks' => 9087,
'position' => '48.44',
'impressions' => 1247506
],
[
'ctr' => '0.75',
'date' => '2022-11-13',
'clicks' => 9952,
'position' => '50.03',
'impressions' => 1326910
],
[
'ctr' => '0.73',
'date' => '2022-11-20',
'clicks' => 9696,
'position' => '50.28',
'impressions' => 1324633
],
[
'ctr' => '0.69',
'date' => '2022-11-27',
'clicks' => 9590,
'position' => '51.03',
'impressions' => 1382602
],
[
'ctr' => '0.71',
'date' => '2022-12-04',
'clicks' => 9691,
'position' => '51.07',
'impressions' => 1365509
],
[
'ctr' => '0.71',
'date' => '2022-12-11',
'clicks' => 9291,
'position' => '51.22',
'impressions' => 1316184
],
[
'ctr' => '0.80',
'date' => '2022-12-18',
'clicks' => 8659,
'position' => '48.20',
'impressions' => 1081944
],
[
'ctr' => '0.75',
'date' => '2022-12-25',
'clicks' => 6449,
'position' => '49.31',
'impressions' => 857591
],
[
'ctr' => '0.66',
'date' => '2023-01-01',
'clicks' => 5822,
'position' => '48.16',
'impressions' => 876828
],
[
'ctr' => '0.77',
'date' => '2023-01-08',
'clicks' => 7501,
'position' => '47.34',
'impressions' => 975764
],
[
'ctr' => '0.90',
'date' => '2023-01-16',
'clicks' => 10148,
'position' => '48.29',
'impressions' => 1125940
]
],
'pages' => [
'topPages' => [
'rows' => $pageRows
],
'paginated' => [
'rows' => $pageRows,
'totals' => [
'page' => 1,
'pages' => 1,
'total' => 10
],
'filters' => [
[
'slug' => 'all',
'name' => 'All',
'active' => true
],
[
'slug' => 'topLosing',
'name' => 'Top Losing',
'active' => false
],
[
'slug' => 'topWinning',
'name' => 'Top Winning',
'active' => false
]
],
'additionalFilters' => [
[
'name' => 'postType',
'options' => [
[
'label' => __( 'All Content Types', 'all-in-one-seo-pack' ),
'value' => ''
]
]
]
]
],
'topLosing' => [
'rows' => $pageRows
],
'topWinning' => [
'rows' => $pageRows
]
]
];
}
/**
* Returns the data for the Keywords.
*
* @since 4.3.0
*
* @param array $args The arguments.
* @return array The data for the Keywords.
*/
public function getKeywordsData( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$keywordsRows = [
[
'ctr' => '4.89',
'clicks' => 5000,
'keyword' => 'best seo plugin',
'position' => '1.93',
'difference' => [
'ctr' => '-1.06',
'decay' => 6590,
'clicks' => -652,
'position' => '0.07',
'impressions' => 7242
],
'impressions' => 102233
],
[
'ctr' => '7.06',
'clicks' => 4404,
'keyword' => 'aioseo is the best',
'position' => '1.32',
'difference' => [
'ctr' => '0.13',
'decay' => 8586,
'clicks' => 633,
'position' => '0.12',
'impressions' => 7953
],
'impressions' => 62357
],
[
'ctr' => '2.81',
'clicks' => 1715,
'keyword' => 'analyze my seo',
'position' => '6.29',
'difference' => [
'ctr' => '-0.03',
'decay' => 13217,
'clicks' => 347,
'position' => '-0.34',
'impressions' => 12870
],
'impressions' => 61102
],
[
'ctr' => '7.46',
'clicks' => 717,
'keyword' => 'wordpress seo',
'position' => '1.18',
'difference' => [
'ctr' => '-0.69',
'decay' => 2729,
'clicks' => 144,
'position' => '-0.08',
'impressions' => 2585
],
'impressions' => 9614
],
[
'ctr' => '6.66',
'clicks' => 446,
'keyword' => 'best seo plugin pro',
'position' => '1.65',
'difference' => [
'ctr' => '0.36',
'decay' => -121,
'clicks' => 16,
'position' => '0.33',
'impressions' => -137
],
'impressions' => 6693
],
[
'ctr' => '7.39',
'clicks' => 409,
'keyword' => 'aioseo wordpress',
'position' => '1.77',
'difference' => [
'ctr' => '-0.39',
'decay' => 534,
'clicks' => 19,
'position' => '-0.13',
'impressions' => 515
],
'impressions' => 5531
],
[
'ctr' => '1.11',
'clicks' => 379,
'keyword' => 'headline analyzer aioseo',
'position' => '8.41',
'difference' => [
'ctr' => '0.43',
'decay' => 134,
'clicks' => 147,
'position' => '-1.36',
'impressions' => -13
],
'impressions' => 34043
],
[
'ctr' => '2.63',
'clicks' => 364,
'keyword' => 'best seo plugin plugin',
'position' => '2.38',
'difference' => [
'ctr' => '0.06',
'decay' => 836,
'clicks' => 29,
'position' => '0.20',
'impressions' => 807
],
'impressions' => 13837
],
[
'ctr' => '1.52',
'clicks' => 326,
'keyword' => 'best seo plugin pack',
'position' => '4.14',
'difference' => [
'ctr' => '-0.19',
'decay' => -1590,
'clicks' => -66,
'position' => '0.64',
'impressions' => -1524
],
'impressions' => 21450
],
[
'ctr' => '6.70',
'clicks' => 264,
'keyword' => 'youtube title analyzer aioseo',
'position' => '7.19',
'difference' => [
'ctr' => '4.73',
'decay' => 3842,
'clicks' => 257,
'position' => '-4.18',
'impressions' => 3585
],
'impressions' => 3940
]
];
return [
'paginated' => [
'rows' => $keywordsRows,
'totals' => [
'page' => 1,
'pages' => 1,
'total' => 10
],
'filters' => [
[
'slug' => 'all',
'name' => 'All',
'active' => true
],
[
'slug' => 'topLosing',
'name' => 'Top Losing',
'active' => false
],
[
'slug' => 'topWinning',
'name' => 'Top Winning',
'active' => false
]
]
],
'topLosing' => $keywordsRows,
'topWinning' => $keywordsRows,
'topKeywords' => $keywordsRows,
'distribution' => [
'top3' => '6.86',
'top10' => '11.03',
'top50' => '52.10',
'top100' => '30.01',
'difference' => [
'top3' => '24.31',
'top10' => '33.70',
'top50' => '-30.50',
'top100' => '-27.51'
]
],
'distributionIntervals' => [
[
'date' => '2022-10-23',
'top3' => '30.70',
'top10' => '38.60',
'top50' => '24.50',
'top100' => '6.20'
],
[
'date' => '2022-10-30',
'top3' => '31.60',
'top10' => '42.10',
'top50' => '21.00',
'top100' => '5.30'
],
[
'date' => '2022-11-06',
'top3' => '31.30',
'top10' => '44.40',
'top50' => '20.30',
'top100' => '4.00'
],
[
'date' => '2022-11-13',
'top3' => '31.70',
'top10' => '44.20',
'top50' => '20.20',
'top100' => '3.90'
],
[
'date' => '2022-11-20',
'top3' => '31.70',
'top10' => '45.70',
'top50' => '18.00',
'top100' => '4.60'
],
[
'date' => '2022-11-27',
'top3' => '32.50',
'top10' => '47.80',
'top50' => '16.80',
'top100' => '2.90'
],
[
'date' => '2022-12-04',
'top3' => '32.50',
'top10' => '47.20',
'top50' => '17.90',
'top100' => '2.40'
],
[
'date' => '2022-12-11',
'top3' => '31.80',
'top10' => '43.70',
'top50' => '21.00',
'top100' => '3.50'
],
[
'date' => '2022-12-18',
'top3' => '30.40',
'top10' => '43.60',
'top50' => '22.40',
'top100' => '3.60'
],
[
'date' => '2022-12-25',
'top3' => '26.90',
'top10' => '37.20',
'top50' => '29.70',
'top100' => '6.20'
],
[
'date' => '2023-01-01',
'top3' => '27.00',
'top10' => '33.80',
'top50' => '31.60',
'top100' => '7.60'
],
[
'date' => '2023-01-08',
'top3' => '26.60',
'top10' => '38.60',
'top50' => '30.00',
'top100' => '4.80'
],
[
'date' => '2023-01-16',
'top3' => '31.10',
'top10' => '43.90',
'top50' => '22.50',
'top100' => '2.50'
]
]
];
}
/**
* Returns the content performance data.
*
* @since 4.7.2
*
* @return array The content performance data.
*/
public function getSeoStatisticsData( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return [];
}
/**
* Returns the Content Rankings data.
*
* @since 4.3.6
*
* @param array $args The arguments.
* @return array The Content Rankings data.
*/
public function getContentRankingsData( $args = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return [
'paginated' => [
'rows' => [
'/' => [
'points' => [
'2022-04' => 13655,
'2022-05' => 12683,
'2022-06' => 13923,
'2022-07' => 13031,
'2022-08' => 10978,
'2022-09' => 9726,
'2022-10' => 13943,
'2022-11' => 21813,
'2022-12' => 11163,
'2023-01' => 4442,
'2023-02' => 4798,
'2023-03' => 5405
],
'page' => '/',
'peak' => 21813,
'decayPercent' => 75,
'decay' => 16407,
'recovering' => false,
'context' => [
'lastUpdated' => 'December 6, 2021'
],
'objectTitle' => 'Homepage',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'objectId' => 0
],
'/high-ranking-page/' => [
'points' => [
'2022-04' => 18426,
'2022-05' => 18435,
'2022-06' => 19764,
'2022-07' => 14769,
'2022-08' => 6486,
'2022-09' => 11984,
'2022-10' => 11539,
'2022-11' => 9939,
'2022-12' => 5340,
'2023-01' => 3965,
'2023-02' => 3799,
'2023-03' => 5440
],
'page' => '/high-ranking-page/',
'peak' => 19764,
'decayPercent' => 72,
'decay' => 14323,
'recovering' => false,
'context' => [
'lastUpdated' => 'November 17, 2022'
],
'objectTitle' => 'High Ranking Page',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'objectId' => 0
],
'/pricing/' => [
'points' => [
'2022-04' => 5356,
'2022-05' => 5425,
'2022-06' => 5165,
'2022-07' => 5479,
'2022-08' => 4995,
'2022-09' => 4466,
'2022-10' => 4545,
'2022-11' => 5361,
'2022-12' => 3092,
'2023-01' => 1948,
'2023-02' => 1630,
'2023-03' => 2341
],
'page' => '/pricing/',
'peak' => 5479,
'decayPercent' => 57,
'decay' => 3137,
'recovering' => false,
'context' => [
'lastUpdated' => 'December 8, 2021'
],
'objectTitle' => 'Pricing',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'objectId' => 0
],
'/features-and-benefits/' => [
'points' => [
'2022-04' => 1272,
'2022-05' => 4151,
'2022-06' => 6953,
'2022-07' => 7785,
'2022-08' => 4177,
'2022-09' => 3378,
'2022-10' => 2553,
'2022-11' => 3971,
'2022-12' => 2143,
'2023-01' => 2335,
'2023-02' => 1666,
'2023-03' => 4892
],
'page' => '/features-and-benefits/',
'peak' => 7785,
'decayPercent' => 37,
'decay' => 2893,
'recovering' => false,
'context' => [
'lastUpdated' => 'February 7, 2022'
],
'objectTitle' => 'Features and Benefits',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'objectId' => 0
],
'/documentation/' => [
'points' => [
'2022-04' => 594,
'2022-05' => 385,
'2022-06' => 337,
'2022-07' => 378,
'2022-08' => 714,
'2022-09' => 2637,
'2022-10' => 2831,
'2022-11' => 2907,
'2022-12' => 1851,
'2023-01' => 277,
'2023-02' => 226,
'2023-03' => 175
],
'page' => '/documentation/',
'peak' => 2907,
'decayPercent' => 93,
'decay' => 2731,
'recovering' => false,
'context' => [
'lastUpdated' => 'January 7, 2022'
],
'objectTitle' => 'Documentation',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'objectId' => 0
],
'/blog/' => [
'points' => [
'2022-04' => 2956,
'2022-05' => 2363,
'2022-06' => 2347,
'2022-07' => 2154,
'2022-08' => 2604,
'2022-09' => 1995,
'2022-10' => 1528,
'2022-11' => 1578,
'2022-12' => 1458,
'2023-01' => 927,
'2023-02' => 629,
'2023-03' => 592
],
'page' => '/blog/',
'peak' => 2956,
'decayPercent' => 79,
'decay' => 2363,
'recovering' => false,
'context' => [
'lastUpdated' => 'April 21, 2022'
],
'objectTitle' => 'Blog',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'objectId' => 0
],
'/blog/my-best-content/' => [
'points' => [
'2022-04' => 1889,
'2022-05' => 1714,
'2022-06' => 2849,
'2022-07' => 4175,
'2022-08' => 5343,
'2022-09' => 6360,
'2022-10' => 6492,
'2022-11' => 6955,
'2022-12' => 6930,
'2023-01' => 5880,
'2023-02' => 5211,
'2023-03' => 4683
],
'page' => '/blog/my-best-content/',
'peak' => 6955,
'decayPercent' => 32,
'decay' => 2272,
'recovering' => false,
'context' => [
'lastUpdated' => 'April 22, 2022'
],
'objectTitle' => 'My Best Content',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'objectId' => 0
],
'/contact-us/' => [
'points' => [
'2022-04' => 3668,
'2022-05' => 3699,
'2022-06' => 4934,
'2022-07' => 5488,
'2022-08' => 5092,
'2022-09' => 5526,
'2022-10' => 4694,
'2022-11' => 4791,
'2022-12' => 3989,
'2023-01' => 4089,
'2023-02' => 4189,
'2023-03' => 4289
],
'page' => '/contact-us/',
'peak' => 5526,
'decayPercent' => 34,
'decay' => 1907,
'recovering' => true,
'context' => [
'lastUpdated' => 'January 28, 2022'
],
'objectTitle' => 'Contact Us',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'objectId' => 0
],
'/support/' => [
'points' => [
'2022-04' => 2715,
'2022-05' => 2909,
'2022-06' => 2981,
'2022-07' => 2988,
'2022-08' => 2586,
'2022-09' => 2592,
'2022-10' => 2391,
'2022-11' => 2446,
'2022-12' => 2045,
'2023-01' => 1239,
'2023-02' => 1077,
'2023-03' => 1198
],
'page' => '/support/',
'peak' => 2988,
'decayPercent' => 59,
'decay' => 1789,
'recovering' => false,
'context' => [
'lastUpdated' => 'February 21, 2021'
],
'objectTitle' => 'Support',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'objectId' => 0
],
'/blog/top-10-contents/' => [
'points' => [
'2022-04' => 1889,
'2022-05' => 1714,
'2022-06' => 2849,
'2022-07' => 4175,
'2022-08' => 5343,
'2022-09' => 6360,
'2022-10' => 6492,
'2022-11' => 6955,
'2022-12' => 6930,
'2023-01' => 5880,
'2023-02' => 5211,
'2023-03' => 4683
],
'page' => '/blog/top-10-contents/',
'peak' => 6955,
'decayPercent' => 32,
'decay' => 2272,
'recovering' => false,
'context' => [
'lastUpdated' => 'October 14, 2022'
],
'objectTitle' => 'Top 10 Contents',
'objectType' => 'post',
'inspectionResult' => $this->getInspectionResult(),
'objectId' => 0
],
],
'totals' => [
'page' => 1,
'pages' => 1,
'total' => 10
],
'additionalFilters' => [
[
'name' => 'postType',
'options' => [
[
'label' => __( 'All Content Types', 'all-in-one-seo-pack' ),
'value' => ''
]
]
]
]
]
];
}
/**
* Get minimum required values for the inspection result.
*
* @since 4.5.0
*
* @return array The inspection result.
*/
private function getInspectionResult() {
$verdicts = [
'PASS',
'FAIL',
'NEUTRAL'
];
return [
'indexStatusResult' => [
'verdict' => $verdicts[ array_rand( $verdicts ) ],
]
];
}
/**
* Clears the Search Statistics cache.
*
* @since 4.5.0
* @version 4.6.2 Moved from Pro to Common.
*
* @return void
*/
public function clearCache() {
aioseo()->core->cache->clearPrefix( 'aioseo_search_statistics_' );
aioseo()->core->cache->clearPrefix( 'search_statistics_' );
}
/**
* Returns all scheduled Search Statistics related actions.
*
* @since 4.6.2
*
* @return array The Search Statistics actions.
*/
protected function getActionSchedulerActions() {
return [
$this->site->action,
$this->sitemap->action
];
}
/**
* Cancels all scheduled Search Statistics related actions.
*
* @since 4.3.3
* @version 4.6.2 Moved from Pro to Common.
*
* @return void
*/
public function cancelActions() {
foreach ( $this->getActionSchedulerActions() as $actionName ) {
as_unschedule_all_actions( $actionName );
}
}
} SearchStatistics/Api/Request.php 0000666 00000022526 15113050717 0012711 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\SearchStatistics\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Constructs requests to our microservice.
*
* @since 4.3.0
* @version 4.6.2 Moved from Pro to Common.
*/
class Request {
/**
* The base API route.
*
* @since 4.3.0
*
* @var string
*/
private $base = '';
/**
* The URL scheme.
*
* @since 4.3.0
*
* @var string
*/
private $scheme = 'https://';
/**
* The current API route.
*
* @since 4.3.0
*
* @var string
*/
private $route = '';
/**
* The full API URL endpoint.
*
* @since 4.3.0
*
* @var string
*/
private $url = '';
/**
* The current API method.
*
* @since 4.3.0
*
* @var string
*/
private $method = '';
/**
* The API token.
*
* @since 4.3.0
*
* @var string
*/
private $token = '';
/**
* The API key.
*
* @since 4.3.0
*
* @var string
*/
private $key = '';
/**
* The API trust token.
*
* @since 4.3.0
*
* @var string
*/
private $tt = '';
/**
* Plugin slug.
*
* @since 4.3.0
*
* @var bool|string
*/
private $plugin = false;
/**
* URL to test connection with.
*
* @since 4.3.0
*
* @var string
*/
private $testurl = '';
/**
* The site URL.
*
* @since 4.3.0
*
* @var string
*/
private $siteurl = '';
/**
* The plugin version.
*
* @since 4.3.0
*
* @var string
*/
private $version = '';
/**
* The site identifier.
*
* @since 4.3.0
*
* @var string
*/
private $sitei = '';
/**
* The request args.
*
* @since 4.3.0
*
* @var array
*/
private $args = [];
/**
* Additional data to append to request body.
*
* @since 4.3.0
*
* @var array
*/
protected $additionalData = [];
/**
* Class constructor.
*
* @since 4.3.0
*
* @param string $route The API route.
* @param array $args List of arguments.
* @param string $method The API method.
*/
public function __construct( $route, $args = [], $method = 'POST' ) {
$this->base = trailingslashit( aioseo()->searchStatistics->api->getApiUrl() ) . trailingslashit( aioseo()->searchStatistics->api->getApiVersion() );
$this->route = trailingslashit( $route );
$this->url = trailingslashit( $this->scheme . $this->base . $this->route );
$this->method = $method;
$this->token = ! empty( $args['token'] ) ? $args['token'] : aioseo()->searchStatistics->api->auth->getToken();
$this->key = ! empty( $args['key'] ) ? $args['key'] : aioseo()->searchStatistics->api->auth->getKey();
$this->tt = ! empty( $args['tt'] ) ? $args['tt'] : '';
$this->args = ! empty( $args ) ? $args : [];
$this->siteurl = site_url();
$this->plugin = 'aioseo-' . strtolower( aioseo()->versionPath );
$this->version = aioseo()->version;
$this->sitei = ! empty( $args['sitei'] ) ? $args['sitei'] : '';
$this->testurl = ! empty( $args['testurl'] ) ? $args['testurl'] : '';
}
/**
* Sends and processes the API request.
*
* @since 4.3.0
*
* @return mixed The response.
*/
public function request() {
// Make sure we're not blocked.
$blocked = $this->isBlocked( $this->url );
if ( is_wp_error( $blocked ) ) {
return new \WP_Error(
'api-error',
sprintf(
'The firewall of the server is blocking outbound calls. Please contact your hosting provider to fix this issue. %s',
$blocked->get_error_message()
)
);
}
// 1. BUILD BODY
$body = [];
if ( ! empty( $this->args ) ) {
foreach ( $this->args as $name => $value ) {
$body[ $name ] = $value;
}
}
foreach ( [ 'sitei', 'siteurl', 'version', 'key', 'token', 'tt' ] as $key ) {
if ( ! empty( $this->{$key} ) ) {
$body[ $key ] = $this->{$key};
}
}
// If this is a plugin API request, add the data.
if ( 'info' === $this->route || 'update' === $this->route ) {
$body['aioseoapi-plugin'] = $this->plugin;
}
// Add in additional data if needed.
if ( ! empty( $this->additionalData ) ) {
$body['aioseoapi-data'] = maybe_serialize( $this->additionalData );
}
if ( 'GET' === $this->method ) {
$body['time'] = time(); // Add a timestamp to avoid caching.
}
$body['timezone'] = gmdate( 'e' );
$body['ip'] = ! empty( $_SERVER['SERVER_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_ADDR'] ) ) : '';
// 2. SET HEADERS
$headers = array_merge( [
'Content-Type' => 'application/json',
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0',
'Pragma' => 'no-cache',
'Expires' => 0,
'AIOSEOAPI-Referer' => site_url(),
'AIOSEOAPI-Sender' => 'WordPress',
'X-AIOSEO-Key' => aioseo()->internalOptions->internal->siteAnalysis->connectToken,
], aioseo()->helpers->getApiHeaders() );
// 3. COMPILE REQUEST DATA
$data = [
'headers' => $headers,
'body' => wp_json_encode( $body ),
'timeout' => 3000,
'user-agent' => aioseo()->helpers->getApiUserAgent()
];
// 4. EXECUTE REQUEST
if ( 'GET' === $this->method ) {
$queryString = http_build_query( $body, '', '&' );
unset( $data['body'] );
$response = wp_remote_get( esc_url_raw( $this->url ) . '?' . $queryString, $data );
} else {
$response = wp_remote_post( esc_url_raw( $this->url ), $data );
}
// 5. VALIDATE RESPONSE
if ( is_wp_error( $response ) ) {
return $response;
}
$responseCode = wp_remote_retrieve_response_code( $response );
$responseBody = json_decode( wp_remote_retrieve_body( $response ), true );
if ( is_wp_error( $responseBody ) ) {
return false;
}
if ( 200 !== $responseCode ) {
$type = ! empty( $responseBody['type'] ) ? $responseBody['type'] : 'api-error';
if ( empty( $responseCode ) ) {
return new \WP_Error(
$type,
'The API was unreachable.'
);
}
if ( empty( $responseBody ) || ( empty( $responseBody['message'] ) && empty( $responseBody['error'] ) ) ) {
return new \WP_Error(
$type,
sprintf(
'The API returned a <strong>%s</strong> response',
$responseCode
)
);
}
if ( ! empty( $responseBody['message'] ) ) {
return new \WP_Error(
$type,
sprintf(
'The API returned a <strong>%1$d</strong> response with this message: <strong>%2$s</strong>',
$responseCode, stripslashes( $responseBody['message'] )
)
);
}
if ( ! empty( $responseBody['error'] ) ) {
return new \WP_Error(
$type,
sprintf(
'The API returned a <strong>%1$d</strong> response with this message: <strong>%2$s</strong>', $responseCode,
stripslashes( $responseBody['error'] )
)
);
}
}
// Check if the trust token is required.
if (
! empty( $this->tt ) &&
( empty( $responseBody['tt'] ) || ! hash_equals( $this->tt, $responseBody['tt'] ) )
) {
return new \WP_Error( 'validation-error', 'Invalid API request.' );
}
return $responseBody;
}
/**
* Sets additional data for the request.
*
* @since 4.3.0
*
* @param array $data The additional data.
* @return void
*/
public function setAdditionalData( array $data ) {
$this->additionalData = array_merge( $this->additionalData, $data );
}
/**
* Checks if the given URL is blocked for a request.
*
* @since 4.3.0
*
* @param string $url The URL to test against.
* @return bool|\WP_Error False or WP_Error if it is blocked.
*/
private function isBlocked( $url = '' ) {
// The below page is a test HTML page used for firewall/router login detection
// and for image linking purposes in Google Images. We use it to test outbound connections
// It's on Google's main CDN so it loads in most countries in 0.07 seconds or less. Perfect for our
// use case of testing outbound connections.
$testurl = ! empty( $this->testurl ) ? $this->testurl : 'https://www.google.com/blank.html';
if ( defined( 'WP_HTTP_BLOCK_EXTERNAL' ) && WP_HTTP_BLOCK_EXTERNAL ) {
if ( defined( 'WP_ACCESSIBLE_HOSTS' ) ) {
$wpHttp = new \WP_Http();
$onBlacklist = $wpHttp->block_request( $url );
if ( $onBlacklist ) {
return new \WP_Error( 'api-error', 'The API was unreachable because the API url is on the WP HTTP blocklist.' );
} else {
$params = [
'sslverify' => false,
'timeout' => 2,
'user-agent' => aioseo()->helpers->getApiUserAgent(),
'body' => ''
];
$response = wp_remote_get( $testurl, $params );
if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 ) {
return false;
} else {
if ( is_wp_error( $response ) ) {
return $response;
} else {
return new \WP_Error( 'api-error', 'The API was unreachable because the call to Google failed.' );
}
}
}
} else {
return new \WP_Error( 'api-error', 'The API was unreachable because no external hosts are allowed on this site.' );
}
} else {
$params = [
'sslverify' => false,
'timeout' => 2,
'user-agent' => aioseo()->helpers->getApiUserAgent(),
'body' => ''
];
$response = wp_remote_get( $testurl, $params );
if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 ) {
return false;
} else {
if ( is_wp_error( $response ) ) {
return $response;
} else {
return new \WP_Error( 'api-error', 'The API was unreachable because the call to Google failed.' );
}
}
}
}
} SearchStatistics/Api/Api.php 0000666 00000004550 15113050717 0011767 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\SearchStatistics\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* API class.
*
* @since 4.3.0
* @version 4.6.2 Moved from Pro to Common.
*/
class Api {
/**
* Holds the instance of the Auth class.
*
* @since 4.3.0
*
* @var Auth
*/
public $auth;
/**
* Holds the instance of the TrustToken class.
*
* @since 4.3.0
*
* @var TrustToken
*/
public $trustToken;
/**
* Holds the instance of the Listener class.
*
* @since 4.3.0
*
* @var Listener
*/
public $listener;
/**
* The base URL for the Search Statistics microservice.
*
* @since 4.3.0
*
* @var string
*/
private $url = 'google.aioseo.com';
/**
* The API version for the Search Statistics microservice.
*
* @since 4.3.0
*
* @var string
*/
private $version = 'v1';
/**
* Class constructor.
*
* @since 4.3.0
*/
public function __construct() {
$this->auth = new Auth();
$this->trustToken = new TrustToken();
$this->listener = new Listener();
}
/**
* Returns the site identifier key according to the WordPress keys.
*
* @since 4.3.0
*
* @return string The site identifier key.
*/
public function getSiteIdentifier() {
$authKey = defined( 'AUTH_KEY' ) ? AUTH_KEY : '';
$secureAuthKey = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
$loggedInKey = defined( 'LOGGED_IN_KEY' ) ? LOGGED_IN_KEY : '';
$siteIdentifier = $authKey . $secureAuthKey . $loggedInKey;
$siteIdentifier = preg_replace( '/[^a-zA-Z0-9]/', '', (string) $siteIdentifier );
$siteIdentifier = sanitize_text_field( $siteIdentifier );
$siteIdentifier = trim( $siteIdentifier );
$siteIdentifier = ( strlen( $siteIdentifier ) > 30 ) ? substr( $siteIdentifier, 0, 30 ) : $siteIdentifier;
return $siteIdentifier;
}
/**
* Returns the URL of the remote endpoint.
*
* @since 4.3.0
*
* @return string The URL.
*/
public function getApiUrl() {
if ( defined( 'AIOSEO_SEARCH_STATISTICS_API_URL' ) ) {
return AIOSEO_SEARCH_STATISTICS_API_URL;
}
return $this->url;
}
/**
* Returns the version of the remote endpoint.
*
* @since 4.3.0
*
* @return string The version.
*/
public function getApiVersion() {
if ( defined( 'AIOSEO_SEARCH_STATISTICS_API_VERSION' ) ) {
return AIOSEO_SEARCH_STATISTICS_API_VERSION;
}
return $this->version;
}
} SearchStatistics/Api/Listener.php 0000666 00000016145 15113050717 0013046 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\SearchStatistics\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended
// phpcs:disable HM.Security.NonceVerification.Recommended
/**
* Class that holds our listeners for the microservice.
*
* @since 4.3.0
* @version 4.6.2 Moved from Pro to Common.
*/
class Listener {
/**
* Class constructor.
*
* @since 4.3.0
*/
public function __construct() {
add_action( 'admin_init', [ $this, 'listenForAuthentication' ] );
add_action( 'admin_init', [ $this, 'listenForReauthentication' ] );
add_action( 'admin_init', [ $this, 'listenForReturningBack' ] );
add_action( 'wp_ajax_nopriv_aioseo_is_installed', [ $this, 'isInstalled' ] );
add_action( 'wp_ajax_nopriv_aioseo_rauthenticate', [ $this, 'reauthenticate' ] );
}
/**
* Listens to the response from the microservice server.
*
* @since 4.3.0
*
* @return void
*/
public function listenForAuthentication() {
if ( empty( $_REQUEST['aioseo-oauth-action'] ) || 'auth' !== $_REQUEST['aioseo-oauth-action'] ) {
return;
}
if (
! aioseo()->access->hasCapability( 'aioseo_search_statistics_settings' ) ||
! aioseo()->access->hasCapability( 'aioseo_general_settings' ) ||
! aioseo()->access->hasCapability( 'aioseo_setup_wizard' )
) {
return;
}
if ( empty( $_REQUEST['tt'] ) || empty( $_REQUEST['key'] ) || empty( $_REQUEST['token'] ) || empty( $_REQUEST['authedsite'] ) ) {
return;
}
if ( ! aioseo()->searchStatistics->api->trustToken->validate( sanitize_text_field( wp_unslash( $_REQUEST['tt'] ) ) ) ) {
return;
}
$profile = [
'key' => sanitize_text_field( wp_unslash( $_REQUEST['key'] ) ),
'token' => sanitize_text_field( wp_unslash( $_REQUEST['token'] ) ),
'siteurl' => site_url(),
'authedsite' => esc_url_raw( wp_unslash( $this->getAuthenticatedDomain() ) )
];
$success = aioseo()->searchStatistics->api->auth->verify( $profile );
if ( ! $success ) {
return;
}
$this->saveAndRedirect( $profile );
}
/**
* Listens to for the reauthentication response from the microservice.
*
* @since 4.3.0
*
* @return void
*/
public function listenForReauthentication() {
if ( empty( $_REQUEST['aioseo-oauth-action'] ) || 'reauth' !== $_REQUEST['aioseo-oauth-action'] ) {
return;
}
if (
! aioseo()->access->hasCapability( 'aioseo_search_statistics_settings' ) ||
! aioseo()->access->hasCapability( 'aioseo_general_settings' ) ||
! aioseo()->access->hasCapability( 'aioseo_setup_wizard' )
) {
return;
}
if ( empty( $_REQUEST['tt'] ) || empty( $_REQUEST['authedsite'] ) ) {
return;
}
if ( ! aioseo()->searchStatistics->api->trustToken->validate( sanitize_text_field( wp_unslash( $_REQUEST['tt'] ) ) ) ) {
return;
}
$existingProfile = aioseo()->searchStatistics->api->auth->getProfile( true );
if ( empty( $existingProfile['key'] ) || empty( $existingProfile['token'] ) ) {
return;
}
$profile = [
'key' => $existingProfile['key'],
'token' => $existingProfile['token'],
'siteurl' => site_url(),
'authedsite' => esc_url_raw( wp_unslash( $this->getAuthenticatedDomain() ) )
];
$this->saveAndRedirect( $profile );
}
/**
* Listens for the response from the microservice when the user returns back.
*
* @since 4.6.2
*
* @return void
*/
public function listenForReturningBack() {
if ( empty( $_REQUEST['aioseo-oauth-action'] ) || 'back' !== $_REQUEST['aioseo-oauth-action'] ) {
return;
}
if (
! aioseo()->access->hasCapability( 'aioseo_search_statistics_settings' ) ||
! aioseo()->access->hasCapability( 'aioseo_general_settings' ) ||
! aioseo()->access->hasCapability( 'aioseo_setup_wizard' )
) {
return;
}
wp_safe_redirect( $this->getRedirectUrl() );
exit;
}
/**
* Return a success status code indicating that the plugin is installed.
*
* @since 4.3.0
*
* @return void
*/
public function isInstalled() {
wp_send_json_success( [
'version' => aioseo()->version,
'pro' => aioseo()->pro
] );
}
/**
* Validate the trust token and tells the microservice that we can reauthenticate.
*
* @since 4.3.0
*
* @return void
*/
public function reauthenticate() {
foreach ( [ 'key', 'token', 'tt' ] as $arg ) {
if ( empty( $_REQUEST[ $arg ] ) ) {
wp_send_json_error( [
'error' => 'authenticate_missing_arg',
'message' => 'Authentication request missing parameter: ' . $arg,
'version' => aioseo()->version,
'pro' => aioseo()->pro
] );
}
}
$trustToken = ! empty( $_REQUEST['tt'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['tt'] ) ) : '';
if ( ! aioseo()->searchStatistics->api->trustToken->validate( $trustToken ) ) {
wp_send_json_error( [
'error' => 'authenticate_invalid_tt',
'message' => 'Invalid TT sent',
'version' => aioseo()->version,
'pro' => aioseo()->pro
] );
}
// If the trust token is validated, send a success response to trigger the regular auth process.
wp_send_json_success();
}
/**
* Saves the authenticated account, clear the existing data and redirect back to the settings page.
*
* @since 4.3.0
*
* @return void
*/
private function saveAndRedirect( $profile ) {
// Reset the search statistics data.
aioseo()->searchStatistics->reset();
// Save the authenticated profile.
aioseo()->searchStatistics->api->auth->setProfile( $profile );
// Reset dismissed alerts.
$dismissedAlerts = aioseo()->settings->dismissedAlerts;
foreach ( $dismissedAlerts as $key => $alert ) {
if ( in_array( $key, [ 'searchConsoleNotConnected', 'searchConsoleSitemapErrors' ], true ) ) {
$dismissedAlerts[ $key ] = false;
}
}
aioseo()->settings->dismissedAlerts = $dismissedAlerts;
// Maybe verifies the site.
aioseo()->searchStatistics->site->maybeVerify();
// Redirects to the original page.
wp_safe_redirect( $this->getRedirectUrl() );
exit;
}
/**
* Returns the authenticated domain.
*
* @since 4.3.0
*
* @return string The authenticated domain.
*/
private function getAuthenticatedDomain() {
if ( empty( $_REQUEST['authedsite'] ) ) {
return '';
}
$authedSite = sanitize_text_field( wp_unslash( $_REQUEST['authedsite'] ) );
if ( false !== aioseo()->helpers->stringIndex( $authedSite, 'sc-domain:' ) ) {
$authedSite = str_replace( 'sc-domain:', '', $authedSite );
}
return $authedSite;
}
/**
* Gets the redirect URL.
*
* @since 4.6.2
*
* @return string The redirect URL.
*/
private function getRedirectUrl() {
$returnTo = ! empty( $_REQUEST['return-to'] ) ? sanitize_key( $_REQUEST['return-to'] ) : '';
$redirectUrl = 'admin.php?page=aioseo';
switch ( $returnTo ) {
case 'webmaster-tools':
$redirectUrl = 'admin.php?page=aioseo-settings#/webmaster-tools?activetool=googleSearchConsole';
break;
case 'setup-wizard':
$redirectUrl = 'index.php?page=aioseo-setup-wizard#/' . aioseo()->standalone->setupWizard->getNextStage();
break;
case 'search-statistics':
$redirectUrl = 'admin.php?page=aioseo-search-statistics/#search-statistics';
break;
}
return admin_url( $redirectUrl );
}
} SearchStatistics/Api/TrustToken.php 0000666 00000003001 15113050717 0013366 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\SearchStatistics\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the trust token.
*
* @since 4.3.0
* @version 4.6.2 Moved from Pro to Common.
*/
class TrustToken {
/**
* Returns the trust token from the database or creates a new one & stores it.
*
* @since 4.3.0
*
* @return string The trust token.
*/
public function get() {
$trustToken = aioseo()->internalOptions->internal->searchStatistics->trustToken;
if ( empty( $trustToken ) ) {
$trustToken = $this->generate();
aioseo()->internalOptions->internal->searchStatistics->trustToken = $trustToken;
}
return $trustToken;
}
/**
* Rotates the trust token.
*
* @since 4.3.0
*
* @return void
*/
public function rotate() {
$trustToken = $this->generate();
aioseo()->internalOptions->internal->searchStatistics->trustToken = $trustToken;
}
/**
* Generates a new trust token.
*
* @since 4.3.0
*
* @return string The trust token.
*/
public function generate() {
return hash( 'sha512', wp_generate_password( 128, true, true ) . uniqid( '', true ) );
}
/**
* Verifies whether the passed trust token is valid or not.
*
* @since 4.3.0
*
* @param string $passedTrustToken The trust token to validate.
* @return bool Whether the trust token is valid or not.
*/
public function validate( $passedTrustToken = '' ) {
$trustToken = $this->get();
return hash_equals( $trustToken, $passedTrustToken );
}
} SearchStatistics/Api/Auth.php 0000666 00000010064 15113050717 0012154 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\SearchStatistics\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the authentication/connection to our microservice.
*
* @since 4.3.0
* @version 4.6.2 Moved from Pro to Common.
*/
class Auth {
/**
* The authenticated profile data.
*
* @since 4.3.0
*
* @var array
*/
private $profile = [];
/**
* The type of authentication.
*
* @since 4.6.2
*
* @var string
*/
public $type = 'lite';
/**
* Class constructor.
*
* @since 4.3.0
*/
public function __construct() {
$this->profile = $this->getProfile();
if ( aioseo()->pro ) {
$this->type = 'pro';
}
}
/**
* Returns the authenticated profile.
*
* @since 4.3.0
*
* @param bool $force Busts the cache and forces an update of the profile data.
* @return array The authenticated profile data.
*/
public function getProfile( $force = false ) {
if ( ! empty( $this->profile ) && ! $force ) {
return $this->profile;
}
$this->profile = aioseo()->internalOptions->internal->searchStatistics->profile;
return $this->profile;
}
/**
* Returns the profile key.
*
* @since 4.3.0
*
* @return string The profile key.
*/
public function getKey() {
return ! empty( $this->profile['key'] ) ? $this->profile['key'] : '';
}
/**
* Returns the profile token.
*
* @since 4.3.0
*
* @return string The profile token.
*/
public function getToken() {
return ! empty( $this->profile['token'] ) ? $this->profile['token'] : '';
}
/**
* Returns the authenticated site.
*
* @since 4.3.0
*
* @return string The authenticated site.
*/
public function getAuthedSite() {
return ! empty( $this->profile['authedsite'] ) ? $this->profile['authedsite'] : '';
}
/**
* Sets the profile data.
*
* @since 4.3.0
*
* @return void
*/
public function setProfile( $data = [] ) {
$this->profile = $data;
aioseo()->internalOptions->internal->searchStatistics->profile = $this->profile;
}
/**
* Deletes the profile data.
*
* @since 4.3.0
*
* @return void
*/
public function deleteProfile() {
$this->setProfile( [] );
}
/**
* Check whether we are connected.
*
* @since 4.3.0
*
* @return bool Whether we are connected or not.
*/
public function isConnected() {
return ! empty( $this->profile['key'] );
}
/**
* Verifies whether the authentication details are valid.
*
* @since 4.3.0
*
* @return bool Whether the data is valid or not.
*/
public function verify( $credentials = [] ) {
$creds = ! empty( $credentials ) ? $credentials : aioseo()->internalOptions->internal->searchStatistics->profile;
if ( empty( $creds['key'] ) ) {
return new \WP_Error( 'validation-error', 'Authentication key is missing.' );
}
$request = new Request( "auth/verify/{$this->type}/", [
'tt' => aioseo()->searchStatistics->api->trustToken->get(),
'key' => $creds['key'],
'token' => $creds['token'],
'testurl' => 'https://' . aioseo()->searchStatistics->api->getApiUrl() . '/v1/test/',
] );
$response = $request->request();
aioseo()->searchStatistics->api->trustToken->rotate();
return ! is_wp_error( $response );
}
/**
* Removes all authentication data.
*
* @since 4.3.0
*
* @return bool Whether the authentication data was deleted or not.
*/
public function delete() {
if ( ! $this->isConnected() ) {
return false;
}
$creds = aioseo()->searchStatistics->api->auth->getProfile( true );
if ( empty( $creds['key'] ) ) {
return false;
}
( new Request( "auth/delete/{$this->type}/", [
'tt' => aioseo()->searchStatistics->api->trustToken->get(),
'key' => $creds['key'],
'token' => $creds['token'],
'testurl' => 'https://' . aioseo()->searchStatistics->api->getApiUrl() . '/v1/test/',
] ) )->request();
aioseo()->searchStatistics->api->trustToken->rotate();
aioseo()->searchStatistics->api->auth->deleteProfile();
aioseo()->searchStatistics->reset();
// Resets the results for the Google meta tag.
aioseo()->options->webmasterTools->google = '';
return true;
}
} Rss.php 0000666 00000034711 15113050717 0006036 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
/**
* Adds content before or after posts in the RSS feed.
*
* @since 4.0.0
*/
class Rss {
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
if ( is_admin() ) {
return;
}
add_filter( 'the_content_feed', [ $this, 'addRssContent' ] );
add_filter( 'the_excerpt_rss', [ $this, 'addRssContentExcerpt' ] );
// If Crawl Cleanup is disabled, return early.
if ( ! aioseo()->options->searchAppearance->advanced->crawlCleanup->enable ) {
return;
}
// Control which feed links are visible.
remove_action( 'wp_head', 'feed_links_extra', 3 );
add_action( 'wp_head', [ $this, 'rssFeedLinks' ], 3 );
if ( ! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->global ) {
add_filter( 'feed_links_show_posts_feed', '__return_false' );
}
if ( ! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->globalComments ) {
add_filter( 'feed_links_show_comments_feed', '__return_false' );
}
// Disable feeds that we no longer want on this site.
add_action( 'wp', [ $this, 'disableFeeds' ], -1000 );
}
/**
* Adds content before or after the RSS excerpt.
*
* @since 4.0.0
*
* @param string $content The post excerpt.
* @return string The post excerpt with prepended/appended content.
*/
public function addRssContentExcerpt( $content ) {
return $this->addRssContent( $content, 'excerpt' );
}
/**
* Adds content before or after the RSS post.
*
* @since 4.0.0
*
* @param string $content The post content.
* @param string $type Type of feed.
* @return string The post content with prepended/appended content.
*/
public function addRssContent( $content, $type = 'complete' ) {
$content = trim( $content );
if ( empty( $content ) ) {
return '';
}
if ( is_feed() ) {
global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
$isHome = is_home();
if ( $isHome ) {
// If this feed is for the static blog page, we must temporarily set "is_home" to false.
// Otherwise any getPost() calls will return the blog page object for every post in the feed.
$wp_query->is_home = false; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
}
$before = aioseo()->tags->replaceTags( aioseo()->options->rssContent->before, get_the_ID() );
$after = aioseo()->tags->replaceTags( aioseo()->options->rssContent->after, get_the_ID() );
if ( $before || $after ) {
if ( 'excerpt' === $type ) {
$content = wpautop( $content );
}
$content = aioseo()->helpers->decodeHtmlEntities( $before ) . $content . aioseo()->helpers->decodeHtmlEntities( $after );
}
// Set back to the original value.
$wp_query->is_home = $isHome; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
}
return $content;
}
/**
* Disable feeds we don't want to have on this site.
*
* @since 4.2.1
*
* @return void
*/
public function disableFeeds() {
$archives = aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->included;
if ( BuddyPressIntegration::isComponentPage() ) {
list( $postType, $suffix ) = explode( '_', aioseo()->standalone->buddyPress->component->templateType );
if (
'feed' === $suffix &&
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->all &&
! in_array( $postType, $archives, true )
) {
$this->redirectRssFeed( BuddyPressIntegration::getComponentArchiveUrl( 'activity' ) );
}
}
if ( ! is_feed() ) {
return;
}
$rssFeed = get_query_var( 'feed' );
$homeUrl = get_home_url();
// Atom feed.
if (
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->atom &&
'atom' === $rssFeed
) {
$this->redirectRssFeed( $homeUrl );
}
// RDF/RSS 1.0 feed.
if (
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->rdf &&
'rdf' === $rssFeed
) {
$this->redirectRssFeed( $homeUrl );
}
// Global feed.
if (
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->global &&
[ 'feed' => 'feed' ] === $GLOBALS['wp_query']->query
) {
$this->redirectRssFeed( $homeUrl );
}
// Global comments feed.
if (
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->globalComments &&
is_comment_feed() &&
! ( is_singular() || is_attachment() )
) {
$this->redirectRssFeed( $homeUrl );
}
// Static blog page feed.
if (
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->staticBlogPage &&
aioseo()->helpers->getBlogPageId() === get_queried_object_id()
) {
$this->redirectRssFeed( $homeUrl );
}
// Post comment feeds.
if (
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->postComments &&
is_comment_feed() &&
is_singular()
) {
$this->redirectRssFeed( $homeUrl );
}
// Attachment feeds.
if (
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->attachments &&
'feed' === $rssFeed &&
get_query_var( 'attachment', false )
) {
$this->redirectRssFeed( $homeUrl );
}
// Author feeds.
if (
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->authors &&
is_author()
) {
$this->redirectRssFeed( get_author_posts_url( (int) get_query_var( 'author' ) ) );
}
// Search results feed.
if (
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->search &&
is_search()
) {
$this->redirectRssFeed( esc_url( trailingslashit( $homeUrl ) . '?s=' . get_search_query() ) );
}
// All post types.
$postType = $this->getTheQueriedPostType();
if (
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->all &&
! in_array( $postType, $archives, true ) &&
is_post_type_archive()
) {
$this->redirectRssFeed( get_post_type_archive_link( $postType ) );
}
// All taxonomies.
$taxonomies = aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->taxonomies->included;
$term = get_queried_object();
if (
is_a( $term, 'WP_Term' ) &&
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->taxonomies->all &&
! in_array( $term->taxonomy, $taxonomies, true ) &&
(
is_category() ||
is_tag() ||
is_tax()
)
) {
$termUrl = get_term_link( $term, $term->taxonomy );
if ( is_wp_error( $termUrl ) ) {
$termUrl = $homeUrl;
}
$this->redirectRssFeed( $termUrl );
}
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
return;
}
// Paginated feed pages. This one is last since we are using a regular expression to validate.
if (
! aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->paginated &&
preg_match( '/(\d+\/|(?<=\/)page\/\d+\/)$/', (string) sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) )
) {
$this->redirectRssFeed( $homeUrl );
}
}
/**
* Get the currently queried post type.
*
* @since 4.2.1
*
* @return string The queried post type.
*/
private function getTheQueriedPostType() {
$postType = get_query_var( 'post_type' );
if ( is_array( $postType ) ) {
$postType = reset( $postType );
}
return $postType;
}
/**
* Redirect the feed to the appropriate URL.
*
* @since 4.2.1
*
* @return void
*/
private function redirectRssFeed( $url ) {
if ( empty( $url ) ) {
return;
}
// Set or remove headers.
header_remove( 'Content-Type' );
header_remove( 'Last-Modified' );
header_remove( 'Expires' );
$cache = 'public, max-age=604800, s-maxage=604800, stale-while-revalidate=120, stale-if-error=14400';
if ( is_user_logged_in() ) {
$cache = 'private, max-age=0';
}
header( 'Cache-Control: ' . $cache, true );
wp_safe_redirect( $url, 301, AIOSEO_PLUGIN_SHORT_NAME );
}
/**
* Rewrite the RSS feed links.
*
* @since 4.2.1
*
* @param array $args The arguments to filter.
* @return void
*/
public function rssFeedLinks( $args ) {
$defaults = [
// Translators: Separator between blog name and feed type in feed links.
'separator' => _x( '-', 'feed link', 'all-in-one-seo-pack' ),
// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Post title.
'singletitle' => __( '%1$s %2$s %3$s Comments Feed', 'all-in-one-seo-pack' ),
// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Category name.
'cattitle' => __( '%1$s %2$s %3$s Category Feed', 'all-in-one-seo-pack' ),
// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Tag name.
'tagtitle' => __( '%1$s %2$s %3$s Tag Feed', 'all-in-one-seo-pack' ),
// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Term name, 4: Taxonomy singular name.
'taxtitle' => __( '%1$s %2$s %3$s %4$s Feed', 'all-in-one-seo-pack' ),
// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Author name.
'authortitle' => __( '%1$s %2$s Posts by %3$s Feed', 'all-in-one-seo-pack' ),
// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Search query.
'searchtitle' => __( '%1$s %2$s Search Results for “%3$s” Feed', 'all-in-one-seo-pack' ),
// Translators: 1 - Blog name, 2 - Separator (raquo), 3 - Post type name.
'posttypetitle' => __( '%1$s %2$s %3$s Feed', 'all-in-one-seo-pack' ),
];
$args = wp_parse_args( $args, $defaults );
$attributes = [
'title' => null,
'href' => null
];
if (
aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->postComments &&
is_singular()
) {
$attributes = $this->getPostCommentsAttributes( $args );
}
$archives = aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->included;
$postType = $this->getTheQueriedPostType();
if (
(
aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->archives->all ||
in_array( $postType, $archives, true )
) &&
is_post_type_archive()
) {
$attributes = $this->getPostTypeArchivesAttributes( $args );
}
// All taxonomies.
$taxonomies = aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->taxonomies->included;
$term = get_queried_object();
if (
$term &&
isset( $term->taxonomy ) &&
(
aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->taxonomies->all ||
in_array( $term->taxonomy, $taxonomies, true )
) &&
(
is_category() ||
is_tag() ||
is_tax()
)
) {
$attributes = $this->getTaxonomiesAttributes( $args, $term );
}
if (
aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->authors &&
is_author()
) {
$attributes = $this->getAuthorAttributes( $args );
}
if (
aioseo()->options->searchAppearance->advanced->crawlCleanup->feeds->search &&
is_search()
) {
$attributes = $this->getSearchAttributes( $args );
}
if ( ! empty( $attributes['title'] ) && ! empty( $attributes['href'] ) ) {
echo '<link rel="alternate" type="application/rss+xml" title="' . esc_attr( $attributes['title'] ) . '" href="' . esc_url( $attributes['href'] ) . '" />' . "\n";
}
}
/**
* Retrieve the attributes for post comments feed.
*
* @since 4.2.1
*
* @param array $args An array of arguments.
* @return array An array of attributes.
*/
private function getPostCommentsAttributes( $args ) {
$id = 0;
$post = get_post( $id );
$title = null;
$href = null;
if (
comments_open() ||
pings_open() ||
0 < $post->comment_count
) {
$title = sprintf(
$args['singletitle'],
get_bloginfo( 'name' ),
$args['separator'],
the_title_attribute( [ 'echo' => false ] )
);
$href = get_post_comments_feed_link( $post->ID );
}
return [
'title' => $title,
'href' => $href
];
}
/**
* Retrieve the attributes for post type archives feed.
*
* @since 4.2.1
*
* @param array $args An array of arguments.
* @return array An array of attributes.
*/
private function getPostTypeArchivesAttributes( $args ) {
$postTypeObject = get_post_type_object( $this->getQueriedPostType() );
$title = sprintf( $args['posttypetitle'], get_bloginfo( 'name' ), $args['separator'], $postTypeObject->labels->name );
$href = get_post_type_archive_feed_link( $postTypeObject->name );
return [
'title' => $title,
'href' => $href
];
}
/**
* Retrieve the attributes for taxonomies feed.
*
* @since 4.2.1
*
* @param array $args An array of arguments.
* @param \WP_Term $term The term.
* @return array An array of attributes.
*/
private function getTaxonomiesAttributes( $args, $term ) {
$title = null;
$href = null;
if ( is_category() ) {
$title = sprintf( $args['cattitle'], get_bloginfo( 'name' ), $args['separator'], $term->name );
$href = get_category_feed_link( $term->term_id );
}
if ( is_tag() ) {
$title = sprintf( $args['tagtitle'], get_bloginfo( 'name' ), $args['separator'], $term->name );
$href = get_tag_feed_link( $term->term_id );
}
if ( is_tax() ) {
$tax = get_taxonomy( $term->taxonomy );
$title = sprintf( $args['taxtitle'], get_bloginfo( 'name' ), $args['separator'], $term->name, $tax->labels->singular_name );
$href = get_term_feed_link( $term->term_id, $term->taxonomy );
}
return [
'title' => $title,
'href' => $href
];
}
/**
* Retrieve the attributes for the author feed.
*
* @since 4.2.1
*
* @param array $args An array of arguments.
* @return array An array of attributes.
*/
private function getAuthorAttributes( $args ) {
$authorId = (int) get_query_var( 'author' );
$title = sprintf( $args['authortitle'], get_bloginfo( 'name' ), $args['separator'], get_the_author_meta( 'display_name', $authorId ) );
$href = get_author_feed_link( $authorId );
return [
'title' => $title,
'href' => $href
];
}
/**
* Retrieve the attributes for the search feed.
*
* @since 4.2.1
*
* @param array $args An array of arguments.
* @return array An array of attributes.
*/
private function getSearchAttributes( $args ) {
$title = sprintf( $args['searchtitle'], get_bloginfo( 'name' ), $args['separator'], get_search_query( false ) );
$href = get_search_feed_link();
return [
'title' => $title,
'href' => $href
];
}
/**
* Get the currently queried post type.
*
* @since 4.2.1
*
* @return string The currently queried post type.
*/
private function getQueriedPostType() {
$postType = get_query_var( 'post_type' );
if ( is_array( $postType ) ) {
$postType = reset( $postType );
}
return $postType;
}
} EmailReports/Mail.php 0000666 00000001163 15113050717 0010552 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\EmailReports;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Mail class.
*
* @since 4.7.2
*/
class Mail {
/**
* Send the email.
*
* @since 4.7.2
*
* @param mixed $to Receiver.
* @param mixed $subject Email subject.
* @param mixed $message Message.
* @param array $headers Email headers.
* @return bool Whether the email was sent successfully.
*/
public function send( $to, $subject, $message, $headers = [ 'Content-Type: text/html; charset=UTF-8' ] ) {
return wp_mail( $to, $subject, $message, $headers );
}
} EmailReports/Summary/Content.php 0000666 00000050371 15113050717 0012744 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\EmailReports\Summary;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Summary content class.
*
* @since 4.7.2
*/
class Content {
/**
* The date range data.
*
* @since 4.7.2
*
* @var array
*/
public $dateRange;
/**
* The SEO Statistics data.
*
* @since 4.7.2
*
* @var array
*/
private $seoStatistics = [];
/**
* The Keywords data.
*
* @since 4.7.2
*
* @var array
*/
private $keywords = [];
/**
* The Search Statistics page URL.
*
* @since 4.7.2
*
* @var string
*/
public $searchStatisticsUrl;
/**
* The featured image placeholder URL.
*
* @since 4.7.3
*
* @var string
*/
public $featuredImagePlaceholder = 'https://static.aioseo.io/report/ste/featured-image-placeholder.png';
/**
* Class constructor.
*
* @since 4.7.2
*
* @param array $dateRange The date range data.
* @return void
*/
public function __construct( $dateRange ) {
$this->dateRange = $dateRange;
$this->searchStatisticsUrl = admin_url( 'admin.php?page=aioseo-search-statistics' );
$this->setSeoStatistics();
$this->setKeywords();
}
/**
* Sets the SEO Statistics data.
*
* @since 4.7.2
*
* @return void
*/
private function setSeoStatistics() {
try {
$seoStatistics = aioseo()->searchStatistics->getSeoStatisticsData( [
'startDate' => gmdate( 'Y-m-d', $this->dateRange['startDateRaw'] ),
'endDate' => gmdate( 'Y-m-d', $this->dateRange['endDateRaw'] ),
'orderBy' => 'clicks',
'orderDir' => 'desc',
'limit' => '5',
'offset' => '0',
'filter' => 'all',
] );
if ( empty( $seoStatistics['data'] ) ) {
return;
}
$this->seoStatistics = $seoStatistics['data'];
} catch ( \Exception $e ) {
// Do nothing.
}
}
/**
* Sets the Keywords data.
*
* @since 4.7.2
*
* @return void
*/
private function setKeywords() {
try {
$keywords = aioseo()->searchStatistics->getKeywordsData( [
'startDate' => gmdate( 'Y-m-d', $this->dateRange['startDateRaw'] ),
'endDate' => gmdate( 'Y-m-d', $this->dateRange['endDateRaw'] ),
'orderBy' => 'clicks',
'orderDir' => 'desc',
'limit' => '5',
'offset' => '0',
'filter' => 'all',
] );
if ( empty( $keywords['data'] ) ) {
return;
}
$this->keywords = $keywords['data'];
} catch ( \Exception $e ) {
// Do nothing.
}
}
/**
* Retrieves the content performance data.
*
* @since 4.7.2
*
* @return array The content performance data or an empty array.
*/
public function getPostsStatistics() {
if ( ! $this->seoStatistics ) {
return [];
}
$result = [
'winning' => [
'url' => add_query_arg( [
'aioseo-scroll' => 'aioseo-search-statistics-post-table',
'aioseo-tab' => 'seo-statistics',
'table-filter' => 'TopWinningPages'
], $this->searchStatisticsUrl ),
'items' => []
],
'losing' => [
'url' => add_query_arg( [
'aioseo-scroll' => 'aioseo-search-statistics-post-table',
'aioseo-tab' => 'seo-statistics',
'table-filter' => 'TopLosingPages'
], $this->searchStatisticsUrl ),
'items' => []
]
];
foreach ( array_slice( $this->seoStatistics['pages']['topWinning']['rows'], 0, 3 ) as $row ) {
$postId = $row['objectId'] ?? 0;
$result['winning']['items'][] = [
'title' => $row['objectTitle'],
'url' => get_permalink( $postId ),
'tru_seo' => aioseo()->helpers->isTruSeoEligible( $postId ) ? $this->parseSeoScore( $row['seoScore'] ?? 0 ) : [],
'clicks' => $this->parseClicks( $row['clicks'] ),
'difference' => [
'clicks' => $this->parseDifference( $row['difference']['clicks'] ?? '' ),
]
];
}
$result['winning']['show_tru_seo'] = ! empty( array_filter( array_column( $result['winning']['items'], 'tru_seo' ) ) );
foreach ( array_slice( $this->seoStatistics['pages']['topLosing']['rows'], 0, 3 ) as $row ) {
$postId = $row['objectId'] ?? 0;
$result['losing']['items'][] = [
'title' => $row['objectTitle'],
'url' => get_permalink( $postId ),
'tru_seo' => aioseo()->helpers->isTruSeoEligible( $postId ) ? $this->parseSeoScore( $row['seoScore'] ?? 0 ) : [],
'clicks' => $this->parseClicks( $row['clicks'] ),
'difference' => [
'clicks' => $this->parseDifference( $row['difference']['clicks'] ?? '' ),
]
];
}
$result['losing']['show_tru_seo'] = ! empty( array_filter( array_column( $result['losing']['items'], 'tru_seo' ) ) );
return $result;
}
/**
* Retrieves the milestones data.
*
* @since 4.7.2
*
* @return array The milestones data or an empty array.
*/
public function getMilestones() { // phpcs:ignore Generic.Files.LineLength.MaxExceeded
$milestones = [];
if ( ! $this->seoStatistics ) {
return $milestones;
}
$currentData = [
'impressions' => $this->seoStatistics['statistics']['impressions'] ?? null,
'clicks' => $this->seoStatistics['statistics']['clicks'] ?? null,
'ctr' => $this->seoStatistics['statistics']['ctr'] ?? null,
'keywords' => $this->seoStatistics['statistics']['keywords'] ?? null,
];
$difference = [
'impressions' => $this->seoStatistics['statistics']['difference']['impressions'] ?? null,
'clicks' => $this->seoStatistics['statistics']['difference']['clicks'] ?? null,
'ctr' => $this->seoStatistics['statistics']['difference']['ctr'] ?? null,
'keywords' => $this->seoStatistics['statistics']['difference']['keywords'] ?? null,
];
if ( is_numeric( $currentData['impressions'] ) && is_numeric( $difference['impressions'] ) ) {
$intDifference = intval( $difference['impressions'] );
$message = esc_html__( 'Your site has received the same number of impressions compared to the previous period.', 'all-in-one-seo-pack' );
if ( $intDifference > 0 ) {
// Translators: 1 - The number of impressions, 2 - The percentage increase.
$message = esc_html__( 'Your site has received %1$s more impressions compared to the previous period, which is a %2$s increase.', 'all-in-one-seo-pack' );
}
if ( $intDifference < 0 ) {
// Translators: 1 - The number of impressions, 2 - The percentage increase.
$message = esc_html__( 'Your site has received %1$s fewer impressions compared to the previous period, which is a %2$s decrease.', 'all-in-one-seo-pack' );
}
if ( false !== strpos( $message, '%1' ) ) {
$percentageDiff = 0 === absint( $currentData['impressions'] )
? 100
: round( ( absint( $intDifference ) / absint( $currentData['impressions'] ) ) * 100, 2 );
$percentageDiff = false !== strpos( $percentageDiff, '.' )
? number_format_i18n( $percentageDiff, count( explode( '.', $percentageDiff ) ) )
: $percentageDiff;
$message = sprintf(
$message,
'<strong>' . aioseo()->helpers->compactNumber( absint( $intDifference ) ) . '</strong>',
'<strong>' . $percentageDiff . '%</strong>'
);
}
$milestones[] = [
'message' => $message,
'background' => '#f0f6ff',
'color' => '#004F9D',
'icon' => 'icon-milestone-impressions'
];
}
if ( is_numeric( $currentData['clicks'] ) && is_numeric( $difference['clicks'] ) ) {
$intDifference = intval( $difference['clicks'] );
$message = esc_html__( 'Your site has received the same number of clicks compared to the previous period.', 'all-in-one-seo-pack' );
if ( $intDifference > 0 ) {
// Translators: 1 - The number of clicks, 2 - The percentage increase.
$message = esc_html__( 'Your site has received %1$s more clicks compared to the previous period, which is a %2$s increase.', 'all-in-one-seo-pack' );
}
if ( $intDifference < 0 ) {
// Translators: 1 - The number of clicks, 2 - The percentage increase.
$message = esc_html__( 'Your site has received %1$s fewer clicks compared to the previous period, which is a %2$s decrease.', 'all-in-one-seo-pack' );
}
if ( false !== strpos( $message, '%1' ) ) {
$percentageDiff = 0 === absint( $currentData['clicks'] )
? 100
: round( ( absint( $intDifference ) / absint( $currentData['clicks'] ) ) * 100, 2 );
$percentageDiff = false !== strpos( $percentageDiff, '.' )
? number_format_i18n( $percentageDiff, count( explode( '.', $percentageDiff ) ) )
: $percentageDiff;
$message = sprintf(
$message,
'<strong>' . aioseo()->helpers->compactNumber( absint( $intDifference ) ) . '</strong>',
'<strong>' . $percentageDiff . '%</strong>'
);
}
$milestones[] = [
'message' => $message,
'background' => '#ecfdf5',
'color' => '#077647',
'icon' => 'icon-milestone-clicks'
];
}
if ( is_numeric( $currentData['ctr'] ) && is_numeric( $difference['ctr'] ) ) {
$intDifference = floatval( $difference['ctr'] );
$message = esc_html__( 'Your site has the same CTR compared to the previous period.', 'all-in-one-seo-pack' );
if ( $intDifference > 0 ) {
// Translators: 1 - The CTR.
$message = esc_html__( 'Your site has a %1$s higher CTR compared to the previous period.', 'all-in-one-seo-pack' );
}
if ( $intDifference < 0 ) {
// Translators: 1 - The CTR.
$message = esc_html__( 'Your site has a %1$s lower CTR compared to the previous period.', 'all-in-one-seo-pack' );
}
if ( false !== strpos( $message, '%1' ) ) {
$message = sprintf(
$message,
'<strong>' . number_format_i18n( abs( $intDifference ), count( explode( '.', $intDifference ) ) ) . '%</strong>'
);
}
$milestones[] = [
'message' => $message,
'background' => '#fffbeb',
'color' => '#be6903',
'icon' => 'icon-milestone-ctr'
];
}
if ( is_numeric( $currentData['keywords'] ) && is_numeric( $difference['keywords'] ) ) {
$intDifference = intval( $difference['keywords'] );
$message = esc_html__( 'Your site ranked for the same number of keywords compared to the previous period.', 'all-in-one-seo-pack' );
if ( $intDifference > 0 ) {
// Translators: 1 - The number of keywords, 2 - The percentage increase.
$message = esc_html__( 'Your site ranked for %1$s more keywords compared to the previous period, which is a %2$s increase.', 'all-in-one-seo-pack' );
}
if ( $intDifference < 0 ) {
// Translators: 1 - The number of keywords, 2 - The percentage increase.
$message = esc_html__( 'Your site ranked for %1$s fewer keywords compared to the previous period, which is a %2$s decrease.', 'all-in-one-seo-pack' );
}
if ( false !== strpos( $message, '%1' ) ) {
$percentageDiff = 0 === absint( $currentData['keywords'] )
? 100
: round( ( absint( $intDifference ) / absint( $currentData['keywords'] ) ) * 100, 2 );
$percentageDiff = false !== strpos( $percentageDiff, '.' )
? number_format_i18n( $percentageDiff, count( explode( '.', $percentageDiff ) ) )
: $percentageDiff;
$message = sprintf(
$message,
'<strong>' . aioseo()->helpers->compactNumber( absint( $intDifference ) ) . '</strong>',
'<strong>' . $percentageDiff . '%</strong>'
);
}
$milestones[] = [
'message' => $message,
'background' => '#fef2f2',
'color' => '#ab2039',
'icon' => 'icon-milestone-keywords'
];
}
return $milestones;
}
/**
* Retrieves the keyword performance data.
*
* @since 4.7.2
*
* @return array The keyword performance data or an empty array.
*/
public function getKeywords() {
if ( ! $this->keywords ) {
return [];
}
$result = [
'winning' => [
'url' => add_query_arg( [
'aioseo-scroll' => 'aioseo-search-statistics-keywords-table',
'aioseo-tab' => 'keyword-rank-tracker',
'tab' => 'AllKeywords',
'table-filter' => 'TopWinningKeywords'
], $this->searchStatisticsUrl ),
'items' => []
],
'losing' => [
'url' => add_query_arg( [
'aioseo-scroll' => 'aioseo-search-statistics-keywords-table',
'aioseo-tab' => 'keyword-rank-tracker',
'tab' => 'AllKeywords',
'table-filter' => 'TopLosingKeywords'
], $this->searchStatisticsUrl ),
'items' => []
]
];
foreach ( array_slice( $this->keywords['topWinning'], 0, 3 ) as $row ) {
$result['winning']['items'][] = [
'title' => $row['keyword'],
'clicks' => $this->parseClicks( $row['clicks'] ),
'difference' => [
'clicks' => $this->parseDifference( $row['difference']['clicks'] ?? '' ),
]
];
}
foreach ( array_slice( $this->keywords['topLosing'], 0, 3 ) as $row ) {
$result['losing']['items'][] = [
'title' => $row['keyword'],
'clicks' => $this->parseClicks( $row['clicks'] ),
'difference' => [
'clicks' => $this->parseDifference( $row['difference']['clicks'] ?? '' ),
]
];
}
return $result;
}
/**
* Retrieves the posts data.
*
* @since 4.7.2
*
* @return array The posts' data.
*/
public function getAioPosts() {
$result = [
'publish' => [],
'optimize' => [],
'cta' => [
'text' => esc_html__( 'Create New Post', 'all-in-one-seo-pack' ),
'url' => admin_url( 'post-new.php' )
],
];
// 1. Retrieve the published posts.
$publishPosts = aioseo()->core->db
->start( 'posts as wp' )
->select( 'wp.ID, wp.post_title, aio.seo_score' )
->join( 'aioseo_posts as aio', 'aio.post_id = wp.ID', 'INNER' )
->whereIn( 'wp.post_type', [ 'post' ] )
->whereIn( 'wp.post_status', [ 'publish' ] )
->orderBy( 'wp.post_date DESC' )
->limit( 5 )
->run()
->result();
if ( $publishPosts ) {
$items = $this->parsePosts( $publishPosts );
$result['publish'] = [
'url' => admin_url( 'edit.php?post_status=publish&post_type=post' ),
'items' => $items,
'show_stats' => ! empty( array_filter( array_column( $items, 'stats' ) ) ),
'show_tru_seo' => ! empty( array_filter( array_column( $items, 'tru_seo' ) ) ),
];
}
// 2. Retrieve the posts to optimize.
$optimizePosts = aioseo()->searchStatistics->getContentRankingsData( [
'endDate' => gmdate( 'Y-m-d', $this->dateRange['endDateRaw'] ),
'orderBy' => 'decayPercent',
'orderDir' => 'asc',
'limit' => '3',
'offset' => '0',
'filter' => 'all',
] );
if ( is_array( $optimizePosts['data']['paginated']['rows'] ?? '' ) ) {
$items = [];
foreach ( array_slice( $optimizePosts['data']['paginated']['rows'], 0, 3 ) as $i => $row ) {
$postId = $row['objectId'] ?? 0;
$items[ $i ] = [
'title' => $row['objectTitle'],
'url' => get_permalink( $postId ),
'image_url' => $this->getThumbnailUrl( $postId ),
'tru_seo' => aioseo()->helpers->isTruSeoEligible( $postId ) ? $this->parseSeoScore( $row['seoScore'] ?? 0 ) : [],
'decay_percent' => $this->parseDifference( $row['decayPercent'] ?? '', true ),
'issues' => [
'url' => add_query_arg( [
'aioseo-tab' => 'post-detail',
'post' => $postId
], $this->searchStatisticsUrl ),
'items' => []
]
];
$aioPost = Models\Post::getPost( $postId );
if ( $aioPost ) {
$items[ $i ]['issues']['items'] = aioseo()->searchStatistics->helpers->getSuggestedChanges( $aioPost );
}
}
$result['optimize'] = [
'url' => add_query_arg( [
'aioseo-tab' => 'content-rankings',
], $this->searchStatisticsUrl ),
'items' => $items,
'show_tru_seo' => ! empty( array_filter( array_column( $items, 'tru_seo' ) ) ),
];
}
return $result;
}
/**
* Retrieves the resources data.
*
* @since 4.7.2
*
* @return array The resources' data.
*/
public function getResources() {
$items = aioseo()->helpers->fetchAioseoArticles( true );
return array_slice( array_filter( $items ), 0, 3 );
}
/**
* Returns if Search Statistics content is allowed.
*
* @since 4.7.3
*
* @return bool Whether Search Statistics content is allowed.
*/
public function allowSearchStatistics() {
static $return = null;
if ( isset( $return ) ) {
return $return;
}
$return = aioseo()->searchStatistics->api->auth->isConnected() &&
aioseo()->license &&
aioseo()->license->hasCoreFeature( 'search-statistics', 'seo-statistics' ) &&
aioseo()->license->hasCoreFeature( 'search-statistics', 'keyword-rankings' );
return $return;
}
/**
* Parses the SEO score.
*
* @since 4.7.2
*
* @param int|string $score The SEO score.
* @return array The parsed SEO score.
*/
private function parseSeoScore( $score ) {
$score = intval( $score );
$parsed = [
'value' => $score,
'color' => '#a1a1a1',
'text' => $score ? "$score/100" : esc_html__( 'N/A', 'all-in-one-seo-pack' ),
];
if ( $parsed['value'] > 79 ) {
$parsed['color'] = '#00aa63';
} elseif ( $parsed['value'] > 49 ) {
$parsed['color'] = '#ff8c00';
} elseif ( $parsed['value'] > 0 ) {
$parsed['color'] = '#df2a4a';
}
return $parsed;
}
/**
* Parses a difference.
*
* @since 4.7.2
*
* @param int|string $number The number to parse.
* @param bool $percentage Whether to return the text result as a percentage.
* @return array The parsed result.
*/
private function parseDifference( $number, $percentage = false ) {
$parsed = [
'color' => '#a1a1a1',
'text' => esc_html__( 'N/A', 'all-in-one-seo-pack' ),
];
if ( ! is_numeric( $number ) ) {
return $parsed;
}
$number = intval( $number );
$parsed['text'] = aioseo()->helpers->compactNumber( absint( $number ) );
if ( $percentage ) {
$parsed['text'] = $number . '%';
}
if ( $number > 0 ) {
$parsed['color'] = '#00aa63';
} elseif ( $number < 0 ) {
$parsed['color'] = '#df2a4a';
}
return $parsed;
}
/**
* Parses the clicks number.
*
* @since 4.7.2
*
* @param float|int|string $number The number of clicks.
* @return string The parsed number of clicks.
*/
private function parseClicks( $number ) {
return aioseo()->helpers->compactNumber( $number );
}
/**
* Parses the posts data.
*
* @since 4.7.2
*
* @param array $posts The posts.
* @return array The parsed posts' data.
*/
private function parsePosts( $posts ) {
$parsed = [];
foreach ( $posts as $k => $item ) {
$parsed[ $k ] = [
'title' => aioseo()->helpers->truncate( $item->post_title, 75 ),
'url' => get_permalink( $item->ID ),
'image_url' => $this->getThumbnailUrl( $item->ID ),
'tru_seo' => aioseo()->helpers->isTruSeoEligible( $item->ID ) ? $this->parseSeoScore( $item->seo_score ?? 0 ) : [],
'stats' => []
];
try {
$statistics = [];
if (
$this->allowSearchStatistics() &&
method_exists( aioseo()->searchStatistics, 'getPostDetailSeoStatisticsData' )
) {
$statistics = aioseo()->searchStatistics->getPostDetailSeoStatisticsData( [
'startDate' => gmdate( 'Y-m-d', $this->dateRange['startDateRaw'] ),
'endDate' => gmdate( 'Y-m-d', $this->dateRange['endDateRaw'] ),
'postId' => $item->ID,
], false );
}
if ( isset( $statistics['data']['statistics']['position'] ) ) {
$parsed[ $k ]['stats'][] = [
'icon' => 'position',
'label' => esc_html__( 'Position', 'all-in-one-seo-pack' ),
'value' => round( floatval( $statistics['data']['statistics']['position'] ) ),
];
}
if ( isset( $statistics['data']['statistics']['ctr'] ) ) {
$value = round( floatval( $statistics['data']['statistics']['ctr'] ), 2 );
$parsed[ $k ]['stats'][] = [
'icon' => 'ctr',
'label' => 'CTR',
'value' => ( number_format_i18n( $value, count( explode( '.', $value ) ) ) ) . '%',
];
}
if ( isset( $statistics['data']['statistics']['impressions'] ) ) {
$parsed[ $k ]['stats'][] = [
'icon' => 'impressions',
'label' => esc_html__( 'Impressions', 'all-in-one-seo-pack' ),
'value' => aioseo()->helpers->compactNumber( $statistics['data']['statistics']['impressions'] ),
];
}
} catch ( \Exception $e ) {
// Do nothing.
}
}
return $parsed;
}
/**
* Retrieves the thumbnail URL.
*
* @since 4.7.2
*
* @param int $postId The post ID.
* @return string The post featured image URL (thumbnail size).
*/
private function getThumbnailUrl( $postId ) {
$imageUrl = get_the_post_thumbnail_url( $postId );
return $imageUrl ?: $this->featuredImagePlaceholder;
}
} EmailReports/Summary/Summary.php 0000666 00000022740 15113050717 0012766 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\EmailReports\Summary;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Summary class.
*
* @since 4.7.2
*/
class Summary {
/**
* The action hook to execute when the event is run.
*
* @since 4.7.2
*
* @var string
*/
public $actionHook = 'aioseo_report_summary';
/**
* Recipient for the email. Multiple recipients can be separated by a comma.
*
* @since 4.7.2
*
* @var string
*/
private $recipient;
/**
* Email chosen frequency. Can be either 'weekly' or 'monthly'.
*
* @since 4.7.2
*
* @var string
*/
private $frequency;
/**
* Class constructor.
*
* @since 4.7.2
*/
public function __construct() {
// No need to run any of this during a WP AJAX request.
if ( wp_doing_ajax() ) {
return;
}
// No need to keep trying scheduling unless on admin.
add_action( 'admin_init', [ $this, 'maybeSchedule' ], 20 );
add_action( $this->actionHook, [ $this, 'cronTrigger' ] );
}
/**
* The summary cron callback.
* Hooked into `{@see self::$actionHook}` action hook.
*
* @since 4.7.2
*
* @param string $frequency The frequency of the email.
* @return void
*/
public function cronTrigger( $frequency ) {
// Keep going only if the feature is enabled.
if (
! aioseo()->options->advanced->emailSummary->enable ||
! apply_filters( 'aioseo_report_summary_enable', true, $frequency )
) {
return;
}
// Get all recipients for the given frequency.
$recipients = wp_list_filter( aioseo()->options->advanced->emailSummary->recipients, [ 'frequency' => $frequency ] );
if ( ! $recipients ) {
return;
}
try {
// Get only the email addresses.
$recipients = array_column( $recipients, 'email' );
$this->run( [
'recipient' => implode( ',', $recipients ),
'frequency' => $frequency,
] );
} catch ( \Exception $e ) {
// Do nothing.
}
}
/**
* Trigger the sending of the summary.
*
* @since 4.7.2
*
* @param array $data All the initial data needed for the summary to be sent.
* @throws \Exception If the email could not be sent.
* @return void
*/
public function run( $data ) {
try {
$this->recipient = $data['recipient'] ?? '';
$this->frequency = $data['frequency'] ?? '';
aioseo()->emailReports->mail->send( $this->getRecipient(), $this->getSubject(), $this->getContentHtml(), $this->getHeaders() );
} catch ( \Exception $e ) {
throw new \Exception( esc_html( $e->getMessage() ), esc_html( $e->getCode() ) );
}
}
/**
* Maybe (re)schedule the summary.
*
* @since 4.7.2
*
* @return void
*/
public function maybeSchedule() {
$allowedFrequencies = $this->getAllowedFrequencies();
// Add at least 6 hours after the day starts.
$addToStart = HOUR_IN_SECONDS * 6;
// Add the timezone offset.
$addToStart -= aioseo()->helpers->getTimeZoneOffset();
// Add a random time offset to avoid all emails being sent at the same time. 1440 * 3 = 3 days range.
$addToStart += aioseo()->helpers->generateRandomTimeOffset( aioseo()->helpers->getSiteDomain( true ), 1440 * 3 ) * MINUTE_IN_SECONDS;
foreach ( $allowedFrequencies as $frequency => $data ) {
aioseo()->actionScheduler->scheduleRecurrent( $this->actionHook, $data['start'] + $addToStart, $data['interval'], compact( 'frequency' ) );
}
}
/**
* Get one or more valid recipients.
*
* @since 4.7.2
*
* @throws \Exception If no valid recipient was set for the email.
* @return string The valid recipients.
*/
private function getRecipient() {
$recipients = array_map( 'trim', explode( ',', $this->recipient ) );
$recipients = array_filter( $recipients, 'is_email' );
if ( empty( $recipients ) ) {
throw new \Exception( 'No valid recipient was set for the email.' ); // Not shown to the user.
}
return implode( ',', $recipients );
}
/**
* Get email subject.
*
* @since 4.7.2
*
* @return string The email subject.
*/
private function getSubject() {
// Translators: 1 - Date range.
$out = esc_html__( 'Your SEO Performance Report for %1$s', 'all-in-one-seo-pack' );
$dateRange = $this->getDateRange();
$suffix = date_i18n( 'F', $dateRange['endDateRaw'] );
if ( 'weekly' === $this->frequency ) {
$suffix = $dateRange['range'];
}
return sprintf( $out, $suffix );
}
/**
* Get content html.
*
* @since 4.7.2
*
* @return string The email content.
*/
private function getContentHtml() { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$dateRange = $this->getDateRange();
$content = new Content( $dateRange );
$upsell = [
'search-statistics' => []
];
$preHeader = sprintf(
// Translators: 1 - The plugin short name ("AIOSEO").
esc_html__( 'Dive into your top-performing pages with %1$s and uncover growth opportunities.', 'all-in-one-seo-pack' ),
AIOSEO_PLUGIN_SHORT_NAME
);
$iconCalendar = 'weekly' === $this->frequency
? 'icon-calendar-weekly'
: 'icon-calendar-monthly';
$heading = 'weekly' === $this->frequency
? esc_html__( 'Your Weekly SEO Email Summary', 'all-in-one-seo-pack' )
: esc_html__( 'Your Monthly SEO Email Summary', 'all-in-one-seo-pack' );
$subheading = 'weekly' === $this->frequency
? esc_html__( 'Let\'s take a look at your SEO updates and content progress this week.', 'all-in-one-seo-pack' )
: esc_html__( 'Let\'s take a look at your SEO updates and content progress this month.', 'all-in-one-seo-pack' );
$statisticsReport = [
'posts' => [],
'keywords' => [],
'milestones' => [],
'cta' => [
'text' => esc_html__( 'See All SEO Statistics', 'all-in-one-seo-pack' ),
'url' => $content->searchStatisticsUrl
],
];
if ( ! $content->allowSearchStatistics() ) {
$upsell['search-statistics'] = [
'cta' => [
'text' => esc_html__( 'Unlock Search Statistics', 'all-in-one-seo-pack' ),
'url' => $content->searchStatisticsUrl,
],
];
}
if ( ! $upsell['search-statistics'] ) {
$subheading = 'weekly' === $this->frequency
? esc_html__( 'Let\'s take a look at how your site has performed in search results this week.', 'all-in-one-seo-pack' )
: esc_html__( 'Let\'s take a look at how your site has performed in search results this month.', 'all-in-one-seo-pack' );
$statisticsReport['posts'] = $content->getPostsStatistics();
$statisticsReport['keywords'] = $content->getKeywords();
$statisticsReport['milestones'] = $content->getMilestones();
}
$mktUrl = trailingslashit( AIOSEO_MARKETING_URL );
$medium = 'email-report-summary';
$posts = $content->getAioPosts();
$resources = [
'posts' => array_map( function ( $item ) use ( $medium, $content ) {
return array_merge( $item, [
'url' => aioseo()->helpers->utmUrl( $item['url'], $medium ),
'image' => [
'url' => ! empty( $item['image']['sizes']['medium']['source_url'] )
? $item['image']['sizes']['medium']['source_url']
: $content->featuredImagePlaceholder
]
] );
}, $content->getResources() ),
'cta' => [
'text' => esc_html__( 'See All Resources', 'all-in-one-seo-pack' ),
'url' => aioseo()->helpers->utmUrl( 'https://aioseo.com/blog/', $medium ),
],
];
$links = [
'disable' => admin_url( 'admin.php?page=aioseo-settings&aioseo-scroll=aioseo-email-summary-row&aioseo-highlight=aioseo-email-summary-row&aioseo-tab=advanced' ),
'update' => admin_url( 'update-core.php' ),
'marketing-site' => aioseo()->helpers->utmUrl( $mktUrl, $medium ),
'facebook' => aioseo()->helpers->utmUrl( $mktUrl . 'plugin/facebook', $medium ),
'linkedin' => aioseo()->helpers->utmUrl( $mktUrl . 'plugin/linkedin', $medium ),
'youtube' => aioseo()->helpers->utmUrl( $mktUrl . 'plugin/youtube', $medium ),
'twitter' => aioseo()->helpers->utmUrl( $mktUrl . 'plugin/twitter', $medium ),
];
ob_start();
require AIOSEO_DIR . '/app/Common/Views/report/summary.php';
return ob_get_clean();
}
/**
* Get email headers.
*
* @since 4.7.2
*
* @return array The email headers.
*/
private function getHeaders() {
return [ 'Content-Type: text/html; charset=UTF-8' ];
}
/**
* Get all allowed frequencies.
*
* @since 4.7.2
*
* @return array The email allowed frequencies.
*/
private function getAllowedFrequencies() {
$time = time();
$secondsTillNow = $time - strtotime( 'today' );
return [
'weekly' => [
'interval' => WEEK_IN_SECONDS,
'start' => strtotime( 'next Monday' ) - $time
],
'monthly' => [
'interval' => MONTH_IN_SECONDS,
'start' => ( strtotime( 'first day of next month' ) + ( DAY_IN_SECONDS * 2 ) - $secondsTillNow ) - $time
]
];
}
/**
* Retrieves the date range data based on the frequency.
*
* @since 4.7.3
*
* @return array The date range data.
*/
private function getDateRange() {
$dateFormat = get_option( 'date_format' );
// If frequency is 'monthly'.
$endDateRaw = strtotime( 'last day of last month' );
$startDateRaw = strtotime( 'first day of last month' );
// If frequency is 'weekly'.
if ( 'weekly' === $this->frequency ) {
$endDateRaw = strtotime( 'last Saturday' );
$startDateRaw = strtotime( 'last Sunday', $endDateRaw );
}
$endDate = date_i18n( $dateFormat, $endDateRaw );
$startDate = date_i18n( $dateFormat, $startDateRaw );
return [
'endDate' => $endDate,
'endDateRaw' => $endDateRaw,
'startDate' => $startDate,
'startDateRaw' => $startDateRaw,
'range' => "$startDate - $endDate",
];
}
} EmailReports/EmailReports.php 0000666 00000004222 15113050717 0012275 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\EmailReports;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* EmailReports class.
*
* @since 4.7.2
*/
class EmailReports {
/**
* Mail object.
*
* @since 4.7.2
*
* @var Mail
*/
public $mail = null;
/**
* Summary object.
*
* @since 4.7.2
*
* @var Summary\Summary
*/
public $summary;
/**
* Class constructor.
*
* @since 4.7.2
*/
public function __construct() {
$this->mail = new Mail();
$this->summary = new Summary\Summary();
add_action( 'aioseo_email_reports_enable_reminder', [ $this, 'enableReminder' ] );
}
/**
* Enable reminder.
*
* @since 4.7.7
*
* @return void
*/
public function enableReminder() {
// User already enabled email reports.
if ( aioseo()->options->advanced->emailSummary->enable ) {
return;
}
// Check if notification exists.
$notification = Models\Notification::getNotificationByName( 'email-reports-enable-reminder' );
if ( $notification->exists() ) {
return;
}
// Add notification.
Models\Notification::addNotification( [
'slug' => uniqid(),
'notification_name' => 'email-reports-enable-reminder',
'title' => __( 'Email Reports', 'all-in-one-seo-pack' ),
'content' => __( 'Stay ahead in SEO with our new email digest! Get the latest tips, trends, and tools delivered right to your inbox, helping you optimize smarter and faster. Enable it today and never miss an update that can take your rankings to the next level.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
'type' => 'info',
'level' => [ 'all' ],
'button1_label' => __( 'Enable Email Reports', 'all-in-one-seo-pack' ),
'button1_action' => 'https://route#aioseo-settings&aioseo-scroll=aioseo-email-summary-row&aioseo-highlight=aioseo-email-summary-row:advanced',
'button2_label' => __( 'All Good, I\'m already getting it', 'all-in-one-seo-pack' ),
'button2_action' => 'http://action#notification/email-reports-enable',
'start' => gmdate( 'Y-m-d H:i:s' )
] );
}
} ImportExport/RankMath/RankMath.php 0000666 00000002063 15113050717 0013150 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\ImportExport;
class RankMath extends ImportExport\Importer {
/**
* A list of plugins to look for to import.
*
* @since 4.0.0
*
* @var array
*/
public $plugins = [
[
'name' => 'Rank Math SEO',
'version' => '1.0',
'basename' => 'seo-by-rank-math/rank-math.php',
'slug' => 'rank-math-seo'
]
];
/**
* Class constructor.
*
* @since 4.0.0
*
* @param ImportExport\ImportExport $importer the ImportExport class.
*/
public function __construct( $importer ) {
$this->helpers = new Helpers();
$this->postMeta = new PostMeta();
$plugins = $this->plugins;
foreach ( $plugins as $key => $plugin ) {
$plugins[ $key ]['class'] = $this;
}
$importer->addPlugins( $plugins );
}
/**
* Imports the settings.
*
* @since 4.0.0
*
* @return void
*/
protected function importSettings() {
new GeneralSettings();
new TitleMeta();
new Sitemap();
}
} ImportExport/RankMath/Helpers.php 0000666 00000007433 15113050717 0013053 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\ImportExport;
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Contains helper methods for the import from Rank Math.
*
* @since 4.0.0
*/
class Helpers extends ImportExport\Helpers {
/**
* Converts the macros from Rank Math to our own smart tags.
*
* @since 4.0.0
*
* @param string $string The string with macros.
* @param string $pageType The page type.
* @return string $string The string with smart tags.
*/
public function macrosToSmartTags( $string, $pageType = null ) {
$macros = $this->getMacros( $pageType );
if ( preg_match( '#%BLOGDESCLINK%#', (string) $string ) ) {
$blogDescriptionLink = '<a href="' .
aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'url' ) ) . '">' .
aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) ) . ' - ' .
aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) ) . '</a>';
$string = str_replace( '%BLOGDESCLINK%', $blogDescriptionLink, $string );
}
if ( preg_match_all( '#%customfield\(([^%\s]*)\)%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
foreach ( $matches[1] as $name ) {
$string = aioseo()->helpers->pregReplace( "#%customfield\($name\)%#", "#custom_field-$name", $string );
}
}
if ( preg_match_all( '#%customterm\(([^%\s]*)\)%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
foreach ( $matches[1] as $name ) {
$string = aioseo()->helpers->pregReplace( "#%customterm\($name\)%#", "#tax_name-$name", $string );
}
}
foreach ( $macros as $macro => $tag ) {
$string = aioseo()->helpers->pregReplace( "#$macro(?![a-zA-Z0-9_])#im", $tag, $string );
}
// Strip out all remaining tags.
$string = aioseo()->helpers->pregReplace( '/%[^\%\s]*\([^\%]*\)%/i', '', aioseo()->helpers->pregReplace( '/%[^\%\s]*%/i', '', $string ) );
return trim( $string );
}
/**
* Returns the macro mappings.
*
* @since 4.1.1
*
* @param string $pageType The page type.
* @return array $macros The macros.
*/
protected function getMacros( $pageType = null ) {
$macros = [
'%sitename%' => '#site_title',
'%blog_title%' => '#site_title',
'%blog_description%' => '#tagline',
'%sitedesc%' => '#tagline',
'%sep%' => '#separator_sa',
'%post_title%' => '#post_title',
'%page_title%' => '#post_title',
'%postname%' => '#post_title',
'%title%' => '#post_title',
'%seo_title%' => '#post_title',
'%excerpt%' => '#post_excerpt',
'%wc_shortdesc%' => '#post_excerpt',
'%category%' => '#taxonomy_title',
'%term%' => '#taxonomy_title',
'%term_description%' => '#taxonomy_description',
'%currentdate%' => '#current_date',
'%currentday%' => '#current_day',
'%currentyear%' => '#current_year',
'%currentmonth%' => '#current_month',
'%name%' => '#author_first_name #author_last_name',
'%author%' => '#author_first_name #author_last_name',
'%date%' => '#post_date',
'%year%' => '#current_year',
'%search_query%' => '#search_term',
// RSS Content tags.
'%AUTHORLINK%' => '#author_link',
'%POSTLINK%' => '#post_link',
'%BLOGLINK%' => '#site_link',
'%FEATUREDIMAGE%' => '#featured_image'
];
switch ( $pageType ) {
case 'archive':
$macros['%title%'] = '#archive_title';
break;
case 'term':
$macros['%title%'] = '#taxonomy_title';
break;
default:
$macros['%title%'] = '#post_title';
break;
}
// Strip all other tags.
$macros['%[^%]*%'] = '';
return $macros;
}
} ImportExport/RankMath/TitleMeta.php 0000666 00000047334 15113050717 0013345 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\ImportExport;
use AIOSEO\Plugin\Common\Models;
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the Search Appearance settings.
*
* @since 4.0.0
*/
class TitleMeta extends ImportExport\SearchAppearance {
/**
* Our robot meta settings.
*
* @since 4.0.0
*/
private $robotMetaSettings = [
'noindex',
'nofollow',
'noarchive',
'noimageindex',
'nosnippet'
];
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
$this->options = get_option( 'rank-math-options-titles' );
if ( empty( $this->options ) ) {
return;
}
$this->migrateHomePageSettings();
$this->migratePostTypeSettings();
$this->migratePostTypeArchiveSettings();
$this->migrateArchiveSettings();
$this->migrateRobotMetaSettings();
$this->migrateKnowledgeGraphSettings();
$this->migrateSocialMetaSettings();
$settings = [
'title_separator' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'separator' ] ],
];
aioseo()->importExport->rankMath->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Migrates the homepage settings.
*
* @since 4.0.0
*
* @return void
*/
private function migrateHomePageSettings() {
if ( isset( $this->options['homepage_title'] ) ) {
aioseo()->options->searchAppearance->global->siteTitle =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['homepage_title'] ) );
}
if ( isset( $this->options['homepage_description'] ) ) {
aioseo()->options->searchAppearance->global->metaDescription =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['homepage_description'] ) );
}
if ( isset( $this->options['homepage_facebook_title'] ) ) {
aioseo()->options->social->facebook->homePage->title = aioseo()->helpers->sanitizeOption( $this->options['homepage_facebook_title'] );
}
if ( isset( $this->options['homepage_facebook_description'] ) ) {
aioseo()->options->social->facebook->homePage->description = aioseo()->helpers->sanitizeOption( $this->options['homepage_facebook_description'] );
}
if ( isset( $this->options['homepage_facebook_image'] ) ) {
aioseo()->options->social->facebook->homePage->image = esc_url( $this->options['homepage_facebook_image'] );
}
}
/**
* Migrates the archive settings.
*
* @since 4.0.0
*
* @return void
*/
private function migrateArchiveSettings() {
$archives = [
'author',
'date'
];
foreach ( $archives as $archive ) {
// Reset existing values first.
foreach ( $this->robotMetaSettings as $robotsMetaName ) {
aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->$robotsMetaName = false;
}
if ( isset( $this->options[ "disable_{$archive}_archives" ] ) ) {
aioseo()->options->searchAppearance->archives->$archive->show = 'off' === $this->options[ "disable_{$archive}_archives" ];
aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->default = 'on' === $this->options[ "disable_{$archive}_archives" ];
aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->noindex = 'on' === $this->options[ "disable_{$archive}_archives" ];
}
if ( isset( $this->options[ "{$archive}_archive_title" ] ) ) {
$value = aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options[ "{$archive}_archive_title" ], 'archive' ) );
if ( 'date' !== $archive ) {
// Archive Title tag needs to be stripped since we don't support it for author archives.
$value = aioseo()->helpers->pregReplace( '/#archive_title/', '', $value );
}
aioseo()->options->searchAppearance->archives->$archive->title = $value;
}
if ( isset( $this->options[ "{$archive}_archive_description" ] ) ) {
aioseo()->options->searchAppearance->archives->$archive->metaDescription =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options[ "{$archive}_archive_description" ], 'archive' ) );
}
if ( ! empty( $this->options[ "{$archive}_custom_robots" ] ) ) {
aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->default = 'off' === $this->options[ "{$archive}_custom_robots" ];
}
if ( ! empty( $this->options[ "{$archive}_robots" ] ) ) {
foreach ( $this->options[ "{$archive}_robots" ] as $robotsName ) {
if ( 'index' === $robotsName ) {
continue;
}
if ( 'noindex' === $robotsName ) {
aioseo()->options->searchAppearance->archives->{$archive}->show = false;
}
aioseo()->options->searchAppearance->archives->{$archive}->advanced->robotsMeta->{$robotsName} = true;
}
}
if ( ! empty( $this->options[ "{$archive}_advanced_robots" ] ) ) {
if ( isset( $this->options[ "{$archive}_advanced_robots" ]['max-snippet'] ) && is_numeric( $this->options[ "{$archive}_advanced_robots" ]['max-snippet'] ) ) {
aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->maxSnippet = intval( $this->options[ "{$archive}_advanced_robots" ]['max-snippet'] );
}
if ( isset( $this->options[ "{$archive}_advanced_robots" ]['max-video-preview'] ) && is_numeric( isset( $this->options[ "{$archive}_advanced_robots" ]['max-video-preview'] ) ) ) {
aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->maxVideoPreview = intval( $this->options[ "{$archive}_advanced_robots" ]['max-video-preview'] );
}
if ( ! empty( $this->options[ "{$archive}_advanced_robots" ]['max-image-preview'] ) ) {
aioseo()->options->searchAppearance->archives->$archive->advanced->robotsMeta->maxImagePreview =
aioseo()->helpers->sanitizeOption( lcfirst( $this->options[ "{$archive}_advanced_robots" ]['max-image-preview'] ) );
}
}
}
if ( isset( $this->options['search_title'] ) ) {
// Archive Title tag needs to be stripped since we don't support it for search archives.
$value = aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['search_title'], 'archive' ) );
aioseo()->options->searchAppearance->archives->search->title = aioseo()->helpers->pregReplace( '/#archive_title/', '', $value );
}
if ( ! empty( $this->options['noindex_search'] ) ) {
aioseo()->options->searchAppearance->archives->search->show = 'off' === $this->options['noindex_search'];
aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->default = 'on' === $this->options['noindex_search'];
aioseo()->options->searchAppearance->archives->search->advanced->robotsMeta->noindex = 'on' === $this->options['noindex_search'];
}
}
/**
* Migrates the post type settings.
*
* @since 4.0.0
*
* @return void
*/
private function migratePostTypeSettings() {
$supportedSettings = [
'title',
'description',
'custom_robots',
'robots',
'advanced_robots',
'default_rich_snippet',
'default_article_type',
'add_meta_box'
];
foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
// Reset existing values first.
foreach ( $this->robotMetaSettings as $robotsMetaName ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->$robotsMetaName = false;
}
foreach ( $this->options as $name => $value ) {
if ( ! preg_match( "#^pt_{$postType}_(.*)$#", (string) $name, $match ) || ! in_array( $match[1], $supportedSettings, true ) ) {
continue;
}
switch ( $match[1] ) {
case 'title':
if ( 'page' === $postType ) {
$value = aioseo()->helpers->pregReplace( '#%category%#', '', $value );
$value = aioseo()->helpers->pregReplace( '#%excerpt%#', '', $value );
}
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->title =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value ) );
break;
case 'description':
if ( 'page' === $postType ) {
$value = aioseo()->helpers->pregReplace( '#%category%#', '', $value );
$value = aioseo()->helpers->pregReplace( '#%excerpt%#', '', $value );
}
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->metaDescription =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value ) );
break;
case 'custom_robots':
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = 'off' === $value;
break;
case 'robots':
if ( ! empty( $value ) ) {
foreach ( $value as $robotsName ) {
if ( 'index' === $robotsName ) {
continue;
}
if ( 'noindex' === $robotsName ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->{$postType}->show = false;
}
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->$robotsName = true;
}
}
break;
case 'advanced_robots':
if ( isset( $value['max-snippet'] ) && is_numeric( $value['max-snippet'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->maxSnippet = intval( $value['max-snippet'] );
}
if ( isset( $value['max-video-preview'] ) && is_numeric( $value['max-video-preview'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->maxVideoPreview = intval( $value['max-video-preview'] );
}
if ( ! empty( $value['max-image-preview'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->maxImagePreview =
aioseo()->helpers->sanitizeOption( $value['max-image-preview'] );
}
break;
case 'add_meta_box':
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox = 'on' === $value;
break;
case 'default_rich_snippet':
$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
if ( 'off' === lcfirst( $value ) || in_array( $postType, [ 'page', 'attachment' ], true ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->schemaType = 'none';
break;
}
if ( in_array( ucfirst( $value ), ImportExport\SearchAppearance::$supportedSchemaGraphs, true ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->schemaType = ucfirst( $value );
}
break;
case 'default_article_type':
if ( in_array( $postType, [ 'page', 'attachment' ], true ) ) {
break;
}
$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
if ( in_array( ucfirst( $value ), ImportExport\SearchAppearance::$supportedArticleGraphs, true ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = ucfirst( $value );
} else {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = 'BlogPosting';
}
break;
default:
break;
}
}
}
}
/**
* Migrates the post type archive settings.
*
* @since 4.0.16
*
* @return void
*/
private function migratePostTypeArchiveSettings() {
$supportedSettings = [
'title',
'description'
];
foreach ( aioseo()->helpers->getPublicPostTypes( true, true ) as $postType ) {
foreach ( $this->options as $name => $value ) {
if ( ! preg_match( "#^pt_{$postType}_archive_(.*)$#", (string) $name, $match ) || ! in_array( $match[1], $supportedSettings, true ) ) {
continue;
}
switch ( $match[1] ) {
case 'title':
aioseo()->dynamicOptions->searchAppearance->archives->$postType->title =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value, 'archive' ) );
break;
case 'description':
aioseo()->dynamicOptions->searchAppearance->archives->$postType->metaDescription =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value, 'archive' ) );
break;
default:
break;
}
}
}
}
/**
* Migrates the robots meta settings.
*
* @since 4.0.0
*
* @return void
*/
private function migrateRobotMetaSettings() {
// Reset existing values first.
foreach ( $this->robotMetaSettings as $robotsMetaName ) {
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->$robotsMetaName = false;
}
if ( ! empty( $this->options['robots_global'] ) ) {
foreach ( $this->options['robots_global'] as $robotsName ) {
if ( 'index' === $robotsName ) {
continue;
}
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default = false;
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->$robotsName = true;
}
}
if ( ! empty( $this->options['advanced_robots_global'] ) ) {
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default = false;
if ( isset( $this->options['robots_global']['max-snippet'] ) && is_numeric( $this->options['robots_global']['max-snippet'] ) ) {
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->maxSnippet = intval( $this->options['robots_global']['max-snippet'] );
}
if ( isset( $this->options['robots_global']['max-video-preview'] ) && is_numeric( $this->options['robots_global']['max-video-preview'] ) ) {
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->maxVideoPreview = intval( $this->options['robots_global']['max-video-preview'] );
}
if ( ! empty( $this->options['robots_global']['max-image-preview'] ) ) {
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->maxImagePreview =
aioseo()->helpers->sanitizeOption( $this->options['robots_global']['max-image-preview'] );
}
}
if ( ! empty( $this->options['noindex_paginated_pages'] ) ) {
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default = false;
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindexPaginated = 'on' === $this->options['noindex_paginated_pages'];
}
}
/**
* Migrates the Knowledge Graph settings.
*
* @since 4.0.0
*
* @return void
*/
private function migrateKnowledgeGraphSettings() {
if ( empty( $this->options['knowledgegraph_type'] ) ) {
return;
}
aioseo()->options->searchAppearance->global->schema->siteRepresents =
'company' === $this->options['knowledgegraph_type'] ? 'organization' : 'person';
if ( ! empty( $this->options['knowledgegraph_name'] ) && 'company' === $this->options['knowledgegraph_type'] ) {
aioseo()->options->searchAppearance->global->schema->organizationName = aioseo()->helpers->sanitizeOption( $this->options['knowledgegraph_name'] );
} elseif ( ! empty( $this->options['knowledgegraph_logo'] ) ) {
aioseo()->options->searchAppearance->global->schema->person = 'manual';
aioseo()->options->searchAppearance->global->schema->personName = aioseo()->helpers->sanitizeOption( $this->options['knowledgegraph_name'] );
}
if ( ! empty( $this->options['knowledgegraph_logo'] ) && 'company' === $this->options['knowledgegraph_type'] ) {
aioseo()->options->searchAppearance->global->schema->organizationLogo = esc_url( $this->options['knowledgegraph_logo'] );
} elseif ( ! empty( $this->options['knowledgegraph_logo'] ) ) {
aioseo()->options->searchAppearance->global->schema->person = 'manual';
aioseo()->options->searchAppearance->global->schema->personLogo = esc_url( $this->options['knowledgegraph_logo'] );
}
$this->migrateKnowledgeGraphPhoneNumber();
}
/**
* Migrates the Knowledge Graph phone number.
*
* @since 4.0.0
*
* @return void
*/
private function migrateKnowledgeGraphPhoneNumber() {
if ( empty( $this->options['phone'] ) ) {
return;
}
$phoneNumber = aioseo()->helpers->sanitizeOption( $this->options['phone'] );
if ( ! preg_match( '#\+\d+#', (string) $phoneNumber ) ) {
$notification = Models\Notification::getNotificationByName( 'v3-migration-schema-number' );
if ( $notification->notification_name ) {
return;
}
Models\Notification::addNotification( [
'slug' => uniqid(),
'notification_name' => 'v3-migration-schema-number',
'title' => __( 'Invalid Phone Number for Knowledge Graph', 'all-in-one-seo-pack' ),
'content' => sprintf(
// Translators: 1 - The phone number.
__( 'We were unable to import the phone number that you previously entered for your Knowledge Graph schema markup.
As it needs to be internationally formatted, please enter it (%1$s) with the country code, e.g. +1 (555) 555-1234.', 'all-in-one-seo-pack' ),
"<strong>$phoneNumber</strong>"
),
'type' => 'warning',
'level' => [ 'all' ],
'button1_label' => __( 'Fix Now', 'all-in-one-seo-pack' ),
'button1_action' => 'http://route#aioseo-search-appearance&aioseo-scroll=schema-graph-phone&aioseo-highlight=schema-graph-phone:schema-markup',
'button2_label' => __( 'Remind Me Later', 'all-in-one-seo-pack' ),
'button2_action' => 'http://action#notification/v3-migration-schema-number-reminder',
'start' => gmdate( 'Y-m-d H:i:s' )
] );
return;
}
aioseo()->options->searchAppearance->global->schema->phone = $phoneNumber;
}
/**
* Migrates the Social Meta settings.
*
* @since 4.0.0
*
* @return void
*/
private function migrateSocialMetaSettings() {
if ( ! empty( $this->options['open_graph_image'] ) ) {
$defaultImage = esc_url( $this->options['open_graph_image'] );
aioseo()->options->social->facebook->general->defaultImagePosts = $defaultImage;
aioseo()->options->social->twitter->general->defaultImagePosts = $defaultImage;
}
if ( ! empty( $this->options['social_url_facebook'] ) ) {
aioseo()->options->social->profiles->urls->facebookPageUrl = esc_url( $this->options['social_url_facebook'] );
}
if ( ! empty( $this->options['facebook_author_urls'] ) ) {
aioseo()->options->social->facebook->advanced->enable = true;
aioseo()->options->social->facebook->advanced->authorUrl = esc_url( $this->options['facebook_author_urls'] );
}
if ( ! empty( $this->options['facebook_admin_id'] ) ) {
aioseo()->options->social->facebook->advanced->enable = true;
aioseo()->options->social->facebook->advanced->adminId = aioseo()->helpers->sanitizeOption( $this->options['facebook_admin_id'] );
}
if ( ! empty( $this->options['facebook_app_id'] ) ) {
aioseo()->options->social->facebook->advanced->enable = true;
aioseo()->options->social->facebook->advanced->appId = aioseo()->helpers->sanitizeOption( $this->options['facebook_app_id'] );
}
if ( ! empty( $this->options['twitter_author_names'] ) ) {
aioseo()->options->social->profiles->urls->twitterUrl =
'https://x.com/' . aioseo()->helpers->sanitizeOption( $this->options['twitter_author_names'] );
}
if ( ! empty( $this->options['twitter_card_type'] ) ) {
preg_match( '#large#', $this->options['twitter_card_type'], $match );
aioseo()->options->social->twitter->general->defaultCardType = ! empty( $match ) ? 'summary_large_image' : 'summary';
}
}
/**
* Migrates the default social image for posts.
*
* @since 4.0.0
*
* @return void
*/
private function migrateDefaultPostSocialImage() {
if ( ! empty( $this->options['open_graph_image'] ) ) {
$defaultImage = esc_url( $this->options['open_graph_image'] );
aioseo()->options->social->facebook->general->defaultImagePosts = $defaultImage;
aioseo()->options->social->twitter->general->defaultImagePosts = $defaultImage;
}
}
} ImportExport/RankMath/PostMeta.php 0000666 00000022432 15113050717 0013201 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Imports the post meta from Rank Math.
*
* @since 4.0.0
*/
class PostMeta {
/**
* The batch import action name.
*
* @since 4.0.0
* @version 4.8.3 Moved from RankMath class to here.
*
* @var string
*/
public $postActionName = 'aioseo_import_post_meta_rank_math';
/**
* The mapped meta
*
* @since 4.8.3
*
* @var array
*/
private $mappedMeta = [
'rank_math_title' => 'title',
'rank_math_description' => 'description',
'rank_math_canonical_url' => 'canonical_url',
'rank_math_focus_keyword' => 'keyphrases',
'rank_math_robots' => '',
'rank_math_advanced_robots' => '',
'rank_math_facebook_title' => 'og_title',
'rank_math_facebook_description' => 'og_description',
'rank_math_facebook_image' => 'og_image_custom_url',
'rank_math_twitter_use_facebook' => 'twitter_use_og',
'rank_math_twitter_title' => 'twitter_title',
'rank_math_twitter_description' => 'twitter_description',
'rank_math_twitter_image' => 'twitter_image_custom_url',
'rank_math_twitter_card_type' => 'twitter_card',
'rank_math_primary_category' => 'primary_term',
'rank_math_pillar_content' => 'pillar_content',
];
/**
* Class constructor.
*
* @since 4.8.3
*/
public function __construct() {
add_action( $this->postActionName, [ $this, 'importPostMeta' ] );
}
/**
* Schedules the post meta import.
*
* @since 4.0.0
*
* @return void
*/
public function scheduleImport() {
try {
if ( as_next_scheduled_action( $this->postActionName ) ) {
return;
}
if ( ! aioseo()->core->cache->get( 'import_post_meta_rank_math' ) ) {
aioseo()->core->cache->update( 'import_post_meta_rank_math', time(), WEEK_IN_SECONDS );
}
as_schedule_single_action( time(), $this->postActionName, [], 'aioseo' );
} catch ( \Exception $e ) {
// Do nothing.
}
}
/**
* Get all posts to be imported
*
* @since 4.8.3
*
* @param int $postsPerAction The number of posts to import per action.
* @return array The posts to be imported.
*/
protected function getPostsToImport( $postsPerAction = 100 ) {
$publicPostTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
$timeStarted = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'import_post_meta_rank_math' ) );
$posts = aioseo()->core->db
->start( 'posts' . ' as p' )
->select( 'p.ID, p.post_type' )
->join( 'postmeta as pm', '`p`.`ID` = `pm`.`post_id`' )
->leftJoin( 'aioseo_posts as ap', '`p`.`ID` = `ap`.`post_id`' )
->whereRaw( "pm.meta_key LIKE 'rank_math_%'" )
->whereRaw( "( p.post_type IN ( '$publicPostTypes' ) )" )
->whereRaw( "( ap.post_id IS NULL OR ap.updated < '$timeStarted' )" )
->orderBy( 'p.ID DESC' )
->groupBy( 'p.ID' )
->limit( $postsPerAction )
->run()
->result();
return $posts;
}
/**
* Imports the post meta.
*
* @since 4.0.0
*
* @return array The posts that were imported.
*/
public function importPostMeta() {
$postsPerAction = apply_filters( 'aioseo_import_rank_math_posts_per_action', 100 );
$posts = $this->getPostsToImport( $postsPerAction );
if ( ! $posts || ! count( $posts ) ) {
aioseo()->core->cache->delete( 'import_post_meta_rank_math' );
return [];
}
foreach ( $posts as $post ) {
$postMeta = aioseo()->core->db
->start( 'postmeta' . ' as pm' )
->select( 'pm.meta_key, pm.meta_value' )
->where( 'pm.post_id', $post->ID )
->whereRaw( "`pm`.`meta_key` LIKE 'rank_math_%'" )
->run()
->result();
$meta = array_merge( [
'post_id' => (int) $post->ID,
], $this->getMetaData( $postMeta, $post ) );
$aioseoPost = Models\Post::getPost( $post->ID );
$aioseoPost->set( $meta );
$aioseoPost->save();
aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );
}
// Clear the Overview cache.
aioseo()->postSettings->clearPostTypeOverviewCache( $posts[0]->ID );
if ( count( $posts ) === $postsPerAction ) {
try {
as_schedule_single_action( time() + 5, $this->postActionName, [], 'aioseo' );
} catch ( \Exception $e ) {
// Do nothing.
}
} else {
aioseo()->core->cache->delete( 'import_post_meta_rank_math' );
}
return $posts;
}
/**
* Get the meta data by post meta.
*
* @since 4.8.3
*
* @param object $postMeta The post meta from database.
* @param object $post The post object.
* @return array The meta data.
*/
public function getMetaData( $postMeta, $post ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$meta = [
'post_id' => $post->ID,
'robots_default' => true,
'robots_noarchive' => false,
'canonical_url' => '',
'robots_nofollow' => false,
'robots_noimageindex' => false,
'robots_noindex' => false,
'robots_noodp' => false,
'robots_nosnippet' => false,
'keyphrases' => [
'focus' => [ 'keyphrase' => '' ],
'additional' => []
],
];
foreach ( $postMeta as $record ) {
$name = $record->meta_key;
$value = $record->meta_value;
if (
! in_array( $post->post_type, [ 'page', 'attachment' ], true ) &&
preg_match( '#^rank_math_schema_([^\s]*)$#', (string) $name, $match ) && ! empty( $match[1] )
) {
switch ( $match[1] ) {
case 'Article':
case 'NewsArticle':
case 'BlogPosting':
$meta['schema_type'] = 'Article';
$meta['schema_type_options'] = wp_json_encode(
[ 'article' => [ 'articleType' => $match[1] ] ]
);
break;
default:
break;
}
}
if ( ! in_array( $name, array_keys( $this->mappedMeta ), true ) ) {
continue;
}
switch ( $name ) {
case 'rank_math_focus_keyword':
$keyphrases = array_map( 'trim', explode( ',', $value ) );
$keyphraseArray = [
'focus' => [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $keyphrases[0] ) ],
'additional' => []
];
unset( $keyphrases[0] );
foreach ( $keyphrases as $keyphrase ) {
$keyphraseArray['additional'][] = [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $keyphrase ) ];
}
$meta['keyphrases'] = $keyphraseArray;
break;
case 'rank_math_robots':
$value = aioseo()->helpers->maybeUnserialize( $value );
if ( ! empty( $value ) ) {
$supportedValues = [ 'index', 'noindex', 'nofollow', 'noarchive', 'noimageindex', 'nosnippet' ];
$meta['robots_default'] = false;
foreach ( $supportedValues as $val ) {
$meta[ "robots_$val" ] = false;
}
// This is a separated foreach as we can import any and all values.
foreach ( $value as $robotsName ) {
$meta[ "robots_$robotsName" ] = true;
}
}
break;
case 'rank_math_advanced_robots':
$value = aioseo()->helpers->maybeUnserialize( $value );
if ( isset( $value['max-snippet'] ) && is_numeric( $value['max-snippet'] ) ) {
$meta['robots_default'] = false;
$meta['robots_max_snippet'] = intval( $value['max-snippet'] );
}
if ( isset( $value['max-video-preview'] ) && is_numeric( $value['max-video-preview'] ) ) {
$meta['robots_default'] = false;
$meta['robots_max_videopreview'] = intval( $value['max-video-preview'] );
}
if ( ! empty( $value['max-image-preview'] ) ) {
$meta['robots_default'] = false;
$meta['robots_max_imagepreview'] = aioseo()->helpers->sanitizeOption( lcfirst( $value['max-image-preview'] ) );
}
break;
case 'rank_math_facebook_image':
$meta['og_image_type'] = 'custom_image';
$meta[ $this->mappedMeta[ $name ] ] = esc_url( $value );
break;
case 'rank_math_twitter_image':
$meta['twitter_image_type'] = 'custom_image';
$meta[ $this->mappedMeta[ $name ] ] = esc_url( $value );
break;
case 'rank_math_twitter_card_type':
preg_match( '#large#', (string) $value, $match );
$meta[ $this->mappedMeta[ $name ] ] = ! empty( $match ) ? 'summary_large_image' : 'summary';
break;
case 'rank_math_twitter_use_facebook':
$meta[ $this->mappedMeta[ $name ] ] = 'on' === $value;
break;
case 'rank_math_primary_category':
$taxonomy = 'category';
$options = new \stdClass();
$options->$taxonomy = (int) $value;
$meta[ $this->mappedMeta[ $name ] ] = wp_json_encode( $options );
break;
case 'rank_math_title':
case 'rank_math_description':
if ( 'page' === $post->post_type ) {
$value = aioseo()->helpers->pregReplace( '#%category%#', '', $value );
$value = aioseo()->helpers->pregReplace( '#%excerpt%#', '', $value );
}
$value = aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $value );
$meta[ $this->mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
break;
case 'rank_math_pillar_content':
$meta['pillar_content'] = 'on' === $value ? 1 : 0;
break;
default:
$meta[ $this->mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
break;
}
}
return $meta;
}
} ImportExport/RankMath/GeneralSettings.php 0000666 00000005777 15113050717 0014560 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the General Settings.
*
* @since 4.0.0
*/
class GeneralSettings {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
$this->options = get_option( 'rank-math-options-general' );
if ( empty( $this->options ) ) {
return;
}
$this->isTruSeoDisabled();
$this->migrateRedirectAttachments();
$this->migrateStripCategoryBase();
$this->migrateRssContentSettings();
$settings = [
'google_verify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'google' ] ],
'bing_verify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'bing' ] ],
'yandex_verify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'yandex' ] ],
'baidu_verify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'baidu' ] ],
'pinterest_verify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'pinterest' ] ],
];
aioseo()->importExport->rankMath->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Checks whether TruSEO should be disabled.
*
* @since 4.0.0
*
* @return void
*/
private function isTruSeoDisabled() {
if ( ! empty( $this->options['frontend_seo_score'] ) ) {
aioseo()->options->advanced->truSeo = 'on' === $this->options['frontend_seo_score'];
}
}
/**
* Migrates the Redirect Attachments setting.
*
* @since 4.0.0
*
* @return void
*/
private function migrateRedirectAttachments() {
if ( isset( $this->options['attachment_redirect_urls'] ) ) {
if ( 'on' === $this->options['attachment_redirect_urls'] ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'attachment_parent';
} else {
aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'disabled';
}
}
}
/**
* Migrates the Strip Category Base setting.
*
* @since 4.2.0
*
* @return void
*/
private function migrateStripCategoryBase() {
if ( isset( $this->options['strip_category_base'] ) ) {
aioseo()->options->searchAppearance->advanced->removeCategoryBase = 'on' === $this->options['strip_category_base'] ? true : false;
}
}
/**
* Migrates the RSS content settings.
*
* @since 4.0.0
*
* @return void
*/
private function migrateRssContentSettings() {
if ( isset( $this->options['rss_before_content'] ) ) {
aioseo()->options->rssContent->before = esc_html( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['rss_before_content'] ) );
}
if ( isset( $this->options['rss_after_content'] ) ) {
aioseo()->options->rssContent->after = esc_html( aioseo()->importExport->rankMath->helpers->macrosToSmartTags( $this->options['rss_after_content'] ) );
}
}
} ImportExport/RankMath/Sitemap.php 0000666 00000011643 15113050717 0013051 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\RankMath;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the sitemap settings.
*
* @since 4.0.0
*/
class Sitemap {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
$this->options = get_option( 'rank-math-options-sitemap' );
if ( empty( $this->options ) ) {
return;
}
$this->migrateIncludedObjects();
$this->migrateIncludeImages();
$this->migrateExcludedPosts();
$this->migrateExcludedTerms();
$settings = [
'items_per_page' => [ 'type' => 'string', 'newOption' => [ 'sitemap', 'general', 'linksPerIndex' ] ],
];
aioseo()->options->sitemap->general->indexes = true;
aioseo()->importExport->rankMath->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Migrates the included post types and taxonomies.
*
* @since 4.0.0
*
* @return void
*/
private function migrateIncludedObjects() {
$includedPostTypes = [];
$includedTaxonomies = [];
$allowedPostTypes = array_values( array_diff( aioseo()->helpers->getPublicPostTypes( true ), aioseo()->helpers->getNoindexedPostTypes() ) );
foreach ( $allowedPostTypes as $postType ) {
foreach ( $this->options as $name => $value ) {
if ( preg_match( "#pt_{$postType}_sitemap$#", (string) $name, $match ) && 'on' === $this->options[ $name ] ) {
$includedPostTypes[] = $postType;
}
}
}
$allowedTaxonomies = array_values( array_diff( aioseo()->helpers->getPublicTaxonomies( true ), aioseo()->helpers->getNoindexedTaxonomies() ) );
foreach ( $allowedTaxonomies as $taxonomy ) {
foreach ( $this->options as $name => $value ) {
if ( preg_match( "#tax_{$taxonomy}_sitemap$#", (string) $name, $match ) && 'on' === $this->options[ $name ] ) {
$includedTaxonomies[] = $taxonomy;
}
}
}
aioseo()->options->sitemap->general->postTypes->included = $includedPostTypes;
if ( count( $allowedPostTypes ) !== count( $includedPostTypes ) ) {
aioseo()->options->sitemap->general->postTypes->all = false;
}
aioseo()->options->sitemap->general->taxonomies->included = $includedTaxonomies;
if ( count( $allowedTaxonomies ) !== count( $includedTaxonomies ) ) {
aioseo()->options->sitemap->general->taxonomies->all = false;
}
}
/**
* Migrates the Redirect Attachments setting.
*
* @since 4.0.0
*
* @return void
*/
private function migrateIncludeImages() {
if ( ! empty( $this->options['include_images'] ) ) {
if ( 'off' === $this->options['include_images'] ) {
aioseo()->options->sitemap->general->advancedSettings->enable = true;
aioseo()->options->sitemap->general->advancedSettings->excludeImages = true;
}
}
}
/**
* Migrates the posts that are excluded from the sitemap.
*
* @since 4.0.0
*
* @return void
*/
private function migrateExcludedPosts() {
if ( empty( $this->options['exclude_posts'] ) ) {
return;
}
$rmExcludedPosts = array_filter( explode( ',', $this->options['exclude_posts'] ) );
$excludedPosts = aioseo()->options->sitemap->general->advancedSettings->excludePosts;
if ( count( $rmExcludedPosts ) ) {
foreach ( $rmExcludedPosts as $rmExcludedPost ) {
$post = get_post( trim( $rmExcludedPost ) );
if ( ! is_object( $post ) ) {
continue;
}
$excludedPost = new \stdClass();
$excludedPost->value = $post->ID;
$excludedPost->type = $post->post_type;
$excludedPost->label = $post->post_title;
$excludedPost->link = get_permalink( $post->ID );
array_push( $excludedPosts, wp_json_encode( $excludedPost ) );
}
aioseo()->options->sitemap->general->advancedSettings->enable = true;
}
aioseo()->options->sitemap->general->advancedSettings->excludePosts = $excludedPosts;
}
/**
* Migrates the terms that are excluded from the sitemap.
*
* @since 4.0.0
*
* @return void
*/
private function migrateExcludedTerms() {
if ( empty( $this->options['exclude_terms'] ) ) {
return;
}
$rmExcludedTerms = array_filter( explode( ',', $this->options['exclude_terms'] ) );
$excludedTerms = aioseo()->options->sitemap->general->advancedSettings->excludeTerms;
if ( count( $rmExcludedTerms ) ) {
foreach ( $rmExcludedTerms as $rmExcludedTerm ) {
$term = get_term( trim( $rmExcludedTerm ) );
if ( ! is_object( $term ) ) {
continue;
}
$excludedTerm = new \stdClass();
$excludedTerm->value = $term->term_id;
$excludedTerm->type = $term->taxonomy;
$excludedTerm->label = $term->name;
$excludedTerm->link = get_term_link( $term );
array_push( $excludedTerms, wp_json_encode( $excludedTerm ) );
}
aioseo()->options->sitemap->general->advancedSettings->enable = true;
}
aioseo()->options->sitemap->general->advancedSettings->excludeTerms = $excludedTerms;
}
} ImportExport/SearchAppearance.php 0000666 00000001535 15113050717 0013126 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Migrates the Search Appearance settings.
*
* @since 4.0.0
*/
abstract class SearchAppearance {
/**
* The schema graphs we support.
*
* @since 4.0.0
*
* @var array
*/
public static $supportedSchemaGraphs = [
'none',
'WebPage',
'Article'
];
/**
* The WebPage graphs we support.
*
* @since 4.0.0
*
* @var array
*/
public static $supportedWebPageGraphs = [
'AboutPage',
'CollectionPage',
'ContactPage',
'FAQPage',
'ItemPage',
'ProfilePage',
'RealEstateListing',
'SearchResultsPage',
'WebPage'
];
/**
* The Article graphs we support.
*
* @since 4.0.0
*
* @var array
*/
public static $supportedArticleGraphs = [
'Article',
'BlogPosting',
'NewsArticle'
];
} ImportExport/Importer.php 0000666 00000002675 15113050717 0011550 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Imports the settings and meta data from other plugins.
*
* @since 4.0.0
*/
abstract class Importer {
/**
* Imports the settings.
*
* @since 4.2.7
*
* @return void
*/
protected function importSettings() {}
/**
* Imports the post meta.
*
* @since 4.2.7
*
* @return void
*/
protected function importPostMeta() {}
/**
* Imports the term meta.
*
* @since 4.2.7
*
* @return void
*/
protected function importTermMeta() {}
/**
* PostMeta class instance.
*
* @since 4.2.7
*
* @var Object
*/
protected $postMeta = null;
/**
* TermMeta class instance.
*
* @since 4.2.7
*
* @var Object
*/
protected $termMeta = null;
/**
* Helpers class instance.
*
* @since 4.2.7
*
* @var Object
*/
public $helpers = null;
/**
* Starts the import.
*
* @since 4.0.0
*
* @param array $options What the user wants to import.
* @return void
*/
public function doImport( $options = [] ) {
if ( empty( $options ) ) {
$this->importSettings();
$this->importPostMeta();
$this->importTermMeta();
return;
}
foreach ( $options as $optionName ) {
switch ( $optionName ) {
case 'settings':
$this->importSettings();
break;
case 'postMeta':
$this->postMeta->scheduleImport();
break;
default:
break;
}
}
}
} ImportExport/ImportExport.php 0000666 00000025310 15113050717 0012412 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Handles the importing/exporting of settings and SEO data.
*
* @since 4.0.0
*/
class ImportExport {
/**
* List of plugins for importing.
*
* @since 4.0.0
*
* @var array
*/
private $plugins = [];
/**
* YoastSeo class instance.
*
* @since 4.2.7
*
* @var YoastSeo\YoastSeo
*/
public $yoastSeo = null;
/**
* RankMath class instance.
*
* @since 4.2.7
*
* @var RankMath\RankMath
*/
public $rankMath = null;
/**
* SeoPress class instance.
*
* @since 4.2.7
*
* @var SeoPress\SeoPress
*/
public $seoPress = null;
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
$this->yoastSeo = new YoastSeo\YoastSeo( $this );
$this->rankMath = new RankMath\RankMath( $this );
$this->seoPress = new SeoPress\SeoPress( $this );
}
/**
* Converts the content of a given V3 .ini settings file to an array of settings.
*
* @since 4.0.0
*
* @param string $contents The .ini file contents.
* @return array The settings.
*/
public function importIniData( $contents ) {
$lines = array_filter( preg_split( '/\r\n|\r|\n/', (string) $contents ) );
$sections = [];
$sectionLabel = '';
$sectionCount = 0;
foreach ( $lines as $line ) {
$line = trim( $line );
// Ignore comments.
if ( preg_match( '#^;.*#', (string) $line ) || preg_match( '#\<(\?php|script)#', (string) $line ) ) {
continue;
}
$matches = [];
if ( preg_match( '#^\[(\S+)\]$#', (string) $line, $label ) ) {
$sectionLabel = strval( $label[1] );
if ( 'post_data' === $sectionLabel ) {
$sectionCount++;
}
if ( ! isset( $sections[ $sectionLabel ] ) ) {
$sections[ $sectionLabel ] = [];
}
} elseif ( preg_match( "#^(\S+)\s*=\s*'(.*)'$#", (string) $line, $matches ) ) {
if ( 'post_data' === $sectionLabel ) {
$sections[ $sectionLabel ][ $sectionCount ][ $matches[1] ] = $matches[2];
} else {
$sections[ $sectionLabel ][ $matches[1] ] = $matches[2];
}
} elseif ( preg_match( '#^(\S+)\s*=\s*NULL$#', (string) $line, $matches ) ) {
if ( 'post_data' === $sectionLabel ) {
$sections[ $sectionLabel ][ $sectionCount ][ $matches[1] ] = '';
} else {
$sections[ $sectionLabel ][ $matches[1] ] = '';
}
} else {
continue;
}
}
$sanitizedSections = [];
foreach ( $sections as $section => $options ) {
$sanitizedSection = [];
foreach ( $options as $option => $value ) {
$sanitizedSection[ $option ] = $this->convertAndSanitize( $value );
}
$sanitizedSections[ $section ] = $sanitizedSection;
}
$oldOptions = [];
$postData = [];
foreach ( $sanitizedSections as $label => $data ) {
switch ( $label ) {
case 'aioseop_options':
$oldOptions = array_merge( $oldOptions, $data );
break;
case 'aiosp_feature_manager_options':
case 'aiosp_opengraph_options':
case 'aiosp_sitemap_options':
case 'aiosp_video_sitemap_options':
case 'aiosp_schema_local_business_options':
case 'aiosp_image_seo_options':
case 'aiosp_robots_options':
case 'aiosp_bad_robots_options':
$oldOptions['modules'][ $label ] = $data;
break;
case 'post_data':
$postData = $data;
break;
default:
break;
}
}
if ( ! empty( $oldOptions ) ) {
aioseo()->migration->migrateSettings( $oldOptions );
}
if ( ! empty( $postData ) ) {
$this->importOldPostMeta( $postData );
}
return true;
}
/**
* Imports the post meta from V3.
*
* @since 4.0.0
*
* @param array $postData The post data.
* @return void
*/
private function importOldPostMeta( $postData ) {
$mappedMeta = [
'_aioseop_title' => 'title',
'_aioseop_description' => 'description',
'_aioseop_custom_link' => 'canonical_url',
'_aioseop_sitemap_exclude' => '',
'_aioseop_disable' => '',
'_aioseop_noindex' => 'robots_noindex',
'_aioseop_nofollow' => 'robots_nofollow',
'_aioseop_sitemap_priority' => 'priority',
'_aioseop_sitemap_frequency' => 'frequency',
'_aioseop_keywords' => 'keywords',
'_aioseop_opengraph_settings' => ''
];
$excludedPosts = [];
$sitemapExcludedPosts = [];
require_once ABSPATH . 'wp-admin/includes/post.php';
foreach ( $postData as $post => $values ) {
$postId = \post_exists( $values['post_title'], '', $values['post_date'] );
if ( ! $postId ) {
continue;
}
$meta = [
'post_id' => $postId,
];
foreach ( $values as $name => $value ) {
if ( ! in_array( $name, array_keys( $mappedMeta ), true ) ) {
continue;
}
switch ( $name ) {
case '_aioseop_sitemap_exclude':
if ( empty( $value ) ) {
break;
}
$sitemapExcludedPosts[] = $postId;
break;
case '_aioseop_disable':
if ( empty( $value ) ) {
break;
}
$excludedPosts[] = $postId;
break;
case '_aioseop_noindex':
case '_aioseop_nofollow':
$meta[ $mappedMeta[ $name ] ] = ! empty( $value );
if ( ! empty( $value ) ) {
$meta['robots_default'] = false;
}
break;
case '_aioseop_keywords':
$meta[ $mappedMeta[ $name ] ] = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $value );
break;
case '_aioseop_opengraph_settings':
$class = new \AIOSEO\Plugin\Common\Migration\Meta();
$meta += $class->convertOpenGraphMeta( $value );
break;
default:
$meta[ $mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
break;
}
}
$post = Models\Post::getPost( $postId );
$post->set( $meta );
$post->save();
}
if ( count( $excludedPosts ) ) {
$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
if ( ! in_array( 'excludePosts', $deprecatedOptions, true ) ) {
array_push( $deprecatedOptions, 'excludePosts' );
aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;
}
$posts = aioseo()->options->deprecated->searchAppearance->advanced->excludePosts;
foreach ( $excludedPosts as $id ) {
if ( ! intval( $id ) ) {
continue;
}
$post = get_post( $id );
if ( ! is_object( $post ) ) {
continue;
}
$excludedPost = new \stdClass();
$excludedPost->type = $post->post_type;
$excludedPost->value = $post->ID;
$excludedPost->label = $post->post_title;
$excludedPost->link = get_permalink( $post );
$posts[] = wp_json_encode( $excludedPost );
}
aioseo()->options->deprecated->searchAppearance->advanced->excludePosts = $posts;
}
if ( count( $sitemapExcludedPosts ) ) {
aioseo()->options->sitemap->general->advancedSettings->enable = true;
$posts = aioseo()->options->sitemap->general->advancedSettings->excludePosts;
foreach ( $sitemapExcludedPosts as $id ) {
if ( ! intval( $id ) ) {
continue;
}
$post = get_post( $id );
if ( ! is_object( $post ) ) {
continue;
}
$excludedPost = new \stdClass();
$excludedPost->type = $post->post_type;
$excludedPost->value = $post->ID;
$excludedPost->label = $post->post_title;
$excludedPost->link = get_permalink( $post );
$posts[] = wp_json_encode( $excludedPost );
}
aioseo()->options->sitemap->general->advancedSettings->excludePosts = $posts;
}
}
/**
* Unserializes an option value if needed and then sanitizes it.
*
* @since 4.0.0
*
* @param string $value The option value.
* @return mixed The sanitized, converted option value.
*/
private function convertAndSanitize( $value ) {
$value = aioseo()->helpers->maybeUnserialize( $value );
switch ( gettype( $value ) ) {
case 'boolean':
return (bool) $value;
case 'string':
return esc_html( wp_strip_all_tags( wp_check_invalid_utf8( trim( $value ) ) ) );
case 'integer':
return intval( $value );
case 'double':
return floatval( $value );
case 'array':
$sanitized = [];
foreach ( (array) $value as $k => $v ) {
$sanitized[ $k ] = $this->convertAndSanitize( $v );
}
return $sanitized;
default:
return '';
}
}
/**
* Starts an import.
*
* @since 4.0.0
*
* @param string $plugin The slug of the plugin to import.
* @param array $settings Which settings to import.
* @return void
*/
public function startImport( $plugin, $settings ) {
// First cancel any scans running that might interfere with our import.
$this->cancelScans();
foreach ( $this->plugins as $pluginData ) {
if ( $pluginData['slug'] === $plugin ) {
$pluginData['class']->doImport( $settings );
return;
}
}
}
/**
* Cancel scans that are currently running and could conflict with our migration.
*
* @since 4.1.4
*
* @return void
*/
private function cancelScans() {
// Figure out how to check if these addons are enabled and then get the action names that way.
aioseo()->actionScheduler->unschedule( 'aioseo_video_sitemap_scan' );
aioseo()->actionScheduler->unschedule( 'aioseo_image_sitemap_scan' );
}
/**
* Checks if an import is currently running.
*
* @since 4.1.4
*
* @return boolean True if an import is currently running.
*/
public function isImportRunning() {
$importsRunning = aioseo()->core->cache->get( 'import_%_meta_%' );
return ! empty( $importsRunning );
}
/**
* Adds plugins to the import/export.
*
* @since 4.0.0
*
* @param array $plugins The plugins to add.
* @return void
*/
public function addPlugins( $plugins ) {
$this->plugins = array_merge( $this->plugins, $plugins );
}
/**
* Get the plugins we allow importing from.
*
* @since 4.0.0
*
* @return array
*/
public function plugins() {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$plugins = [];
$installedPlugins = array_keys( get_plugins() );
foreach ( $this->plugins as $importerPlugin ) {
$data = [
'slug' => $importerPlugin['slug'],
'name' => $importerPlugin['name'],
'version' => null,
'canImport' => false,
'basename' => $importerPlugin['basename'],
'installed' => false
];
if ( in_array( $importerPlugin['basename'], $installedPlugins, true ) ) {
$pluginData = get_file_data( trailingslashit( WP_PLUGIN_DIR ) . $importerPlugin['basename'], [
'name' => 'Plugin Name',
'version' => 'Version',
] );
$canImport = false;
if ( version_compare( $importerPlugin['version'], $pluginData['version'], '<=' ) ) {
$canImport = true;
}
$data['name'] = $pluginData['name'];
$data['version'] = $pluginData['version'];
$data['canImport'] = $canImport;
$data['installed'] = true;
}
$plugins[] = $data;
}
return $plugins;
}
} ImportExport/YoastSeo/Helpers.php 0000666 00000011315 15113050717 0013106 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\ImportExport;
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Contains helper methods for the import from Rank Math.
*
* @since 4.0.0
*/
class Helpers extends ImportExport\Helpers {
/**
* Converts the macros from Yoast SEO to our own smart tags.
*
* @since 4.0.0
*
* @param string $string The string with macros.
* @param string $postType The post type.
* @param string $pageType The page type.
* @return string $string The string with smart tags.
*/
public function macrosToSmartTags( $string, $postType = null, $pageType = null ) {
$macros = $this->getMacros( $postType, $pageType );
if ( preg_match( '#%%BLOGDESCLINK%%#', (string) $string ) ) {
$blogDescriptionLink = '<a href="' .
aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'url' ) ) . '">' .
aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) ) . ' - ' .
aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'description' ) ) . '</a>';
$string = str_replace( '%%BLOGDESCLINK%%', $blogDescriptionLink, $string );
}
if ( preg_match_all( '#%%cf_([^%]*)%%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
foreach ( $matches[1] as $name ) {
if ( ! preg_match( '#\s#', (string) $name ) ) {
$string = aioseo()->helpers->pregReplace( "#%%cf_$name%%#", "#custom_field-$name", $string );
}
}
}
if ( preg_match_all( '#%%tax_([^%]*)%%#', (string) $string, $matches ) && ! empty( $matches[1] ) ) {
foreach ( $matches[1] as $name ) {
if ( ! preg_match( '#\s#', (string) $name ) ) {
$string = aioseo()->helpers->pregReplace( "#%%tax_$name%%#", "#tax_name-$name", $string );
}
}
}
foreach ( $macros as $macro => $tag ) {
$string = aioseo()->helpers->pregReplace( "#$macro(?![a-zA-Z0-9_])#im", $tag, $string );
}
// Strip out all remaining tags.
$string = aioseo()->helpers->pregReplace( '/%[^\%\s]*\([^\%]*\)%/i', '', aioseo()->helpers->pregReplace( '/%[^\%\s]*%/i', '', $string ) );
return trim( $string );
}
/**
* Returns the macro mappings.
*
* @since 4.1.1
*
* @param string $postType The post type.
* @param string $pageType The page type.
* @return array $macros The macros.
*/
protected function getMacros( $postType = null, $pageType = null ) {
$macros = [
'%%sitename%%' => '#site_title',
'%%sitedesc%%' => '#tagline',
'%%sep%%' => '#separator_sa',
'%%term_title%%' => '#taxonomy_title',
'%%term_description%%' => '#taxonomy_description',
'%%category_description%%' => '#taxonomy_description',
'%%tag_description%%' => '#taxonomy_description',
'%%primary_category%%' => '#taxonomy_title',
'%%archive_title%%' => '#archive_title',
'%%pagenumber%%' => '#page_number',
'%%caption%%' => '#attachment_caption',
'%%name%%' => '#author_first_name #author_last_name',
'%%user_description%%' => '#author_bio',
'%%date%%' => '#archive_date',
'%%currentday%%' => '#current_day',
'%%currentmonth%%' => '#current_month',
'%%currentyear%%' => '#current_year',
'%%searchphrase%%' => '#search_term',
'%%AUTHORLINK%%' => '#author_link',
'%%POSTLINK%%' => '#post_link',
'%%BLOGLINK%%' => '#site_link',
'%%category%%' => '#categories',
'%%parent_title%%' => '#parent_title',
'%%wc_sku%%' => '#woocommerce_sku',
'%%wc_price%%' => '#woocommerce_price',
'%%wc_brand%%' => '#woocommerce_brand',
'%%excerpt%%' => '#post_excerpt',
'%%excerpt_only%%' => '#post_excerpt_only'
/* '%%tag%%' => '',
'%%id%%' => '',
'%%page%%' => '',
'%%modified%%' => '',
'%%pagetotal%%' => '',
'%%focuskw%%' => '',
'%%term404%%' => '',
'%%ct_desc_[^%]*%%' => '' */
];
if ( $postType ) {
$postType = get_post_type_object( $postType );
if ( ! empty( $postType ) ) {
$macros += [
'%%pt_single%%' => $postType->labels->singular_name,
'%%pt_plural%%' => $postType->labels->name,
];
}
}
switch ( $pageType ) {
case 'archive':
$macros['%%title%%'] = '#archive_title';
break;
case 'term':
$macros['%%title%%'] = '#taxonomy_title';
break;
default:
$macros['%%title%%'] = '#post_title';
break;
}
// Strip all other tags.
$macros['%%[^%]*%%'] = '';
return $macros;
}
} ImportExport/YoastSeo/SearchAppearance.php 0000666 00000034145 15113050717 0014677 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\ImportExport;
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the Search Appearance settings.
*
* @since 4.0.0
*/
class SearchAppearance {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Whether the homepage social settings have been imported here.
*
* @since 4.2.4
*
* @var bool
*/
public $hasImportedHomepageSocialSettings = false;
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
$this->options = get_option( 'wpseo_titles' );
if ( empty( $this->options ) ) {
return;
}
$this->migrateSeparator();
$this->migrateTitleFormats();
$this->migrateDescriptionFormats();
$this->migrateNoindexSettings();
$this->migratePostTypeSettings();
$this->migratePostTypeArchiveSettings();
$this->migrateRedirectAttachments();
$this->migrateKnowledgeGraphSettings();
$this->migrateRssContentSettings();
$this->migrateStripCategoryBase();
$this->migrateHomepageSocialSettings();
}
/**
* Migrates the title/description separator.
*
* @since 4.0.0
*
* @return void
*/
private function migrateSeparator() {
$separators = [
'sc-dash' => '-',
'sc-ndash' => '–',
'sc-mdash' => '—',
'sc-colon' => ':',
'sc-middot' => '·',
'sc-bull' => '•',
'sc-star' => '*',
'sc-smstar' => '⋆',
'sc-pipe' => '|',
'sc-tilde' => '~',
'sc-laquo' => '«',
'sc-raquo' => '»',
'sc-lt' => '<',
'sc-gt' => '>',
];
if ( ! empty( $this->options['separator'] ) && in_array( $this->options['separator'], array_keys( $separators ), true ) ) {
aioseo()->options->searchAppearance->global->separator = $separators[ $this->options['separator'] ];
}
}
/**
* Migrates the title formats.
*
* @since 4.0.0
*
* @return void
*/
private function migrateTitleFormats() {
aioseo()->options->searchAppearance->global->siteTitle =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['title-home-wpseo'] ) );
aioseo()->options->searchAppearance->archives->date->title =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['title-archive-wpseo'], null, 'archive' ) );
// Archive Title tag needs to be stripped since we don't support it for these two archives.
$value = aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['title-author-wpseo'], null, 'archive' ) );
aioseo()->options->searchAppearance->archives->author->title = aioseo()->helpers->pregReplace( '/#archive_title/', '', $value );
$value = aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['title-search-wpseo'], null, 'archive' ) );
aioseo()->options->searchAppearance->archives->search->title = aioseo()->helpers->pregReplace( '/#archive_title/', '', $value );
}
/**
* Migrates the description formats.
*
* @since 4.0.0
*
* @return void
*/
private function migrateDescriptionFormats() {
$settings = [
'metadesc-home-wpseo' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'metaDescription' ] ],
'metadesc-author-wpseo' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'metaDescription' ] ],
'metadesc-archive-wpseo' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'metaDescription' ] ],
];
aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options, true );
}
/**
* Migrates the noindex settings.
*
* @since 4.0.0
*
* @return void
*/
private function migrateNoindexSettings() {
if ( ! empty( $this->options['noindex-author-wpseo'] ) ) {
aioseo()->options->searchAppearance->archives->author->show = false;
aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default = false;
aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex = true;
} else {
aioseo()->options->searchAppearance->archives->author->show = true;
}
if ( ! empty( $this->options['noindex-archive-wpseo'] ) ) {
aioseo()->options->searchAppearance->archives->date->show = false;
aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default = false;
aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex = true;
} else {
aioseo()->options->searchAppearance->archives->date->show = true;
}
}
/**
* Migrates the post type settings.
*
* @since 4.0.0
*
* @return void
*/
private function migratePostTypeSettings() {
$supportedSettings = [
'title',
'metadesc',
'noindex',
'display-metabox-pt',
'schema-page-type',
'schema-article-type'
];
foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
foreach ( $this->options as $name => $value ) {
if ( ! preg_match( "#(.*)-$postType$#", (string) $name, $match ) || ! in_array( $match[1], $supportedSettings, true ) ) {
continue;
}
switch ( $match[1] ) {
case 'title':
if ( 'page' === $postType ) {
$value = aioseo()->helpers->pregReplace( '#%%primary_category%%#', '', $value );
$value = aioseo()->helpers->pregReplace( '#%%excerpt%%#', '', $value );
}
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->title =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, $postType ) );
break;
case 'metadesc':
if ( 'page' === $postType ) {
$value = aioseo()->helpers->pregReplace( '#%%primary_category%%#', '', $value );
$value = aioseo()->helpers->pregReplace( '#%%excerpt%%#', '', $value );
}
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->metaDescription =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, $postType ) );
break;
case 'noindex':
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->show = empty( $value ) ? true : false;
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = empty( $value ) ? true : false;
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = empty( $value ) ? false : true;
break;
case 'display-metabox-pt':
if ( empty( $value ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox = false;
}
break;
case 'schema-page-type':
$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
if ( in_array( $postType, [ 'post', 'page', 'attachment' ], true ) ) {
break;
}
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->schemaType = 'WebPage';
if ( in_array( $value, ImportExport\SearchAppearance::$supportedWebPageGraphs, true ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->webPageType = $value;
}
break;
case 'schema-article-type':
$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
if ( 'none' === lcfirst( $value ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = 'none';
break;
}
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = 'Article';
if ( in_array( $value, ImportExport\SearchAppearance::$supportedArticleGraphs, true ) ) {
if ( ! in_array( $postType, [ 'page', 'attachment' ], true ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = $value;
}
} else {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->articleType = 'BlogPosting';
}
break;
default:
break;
}
}
}
}
/**
* Migrates the post type archive settings.
*
* @since 4.0.16
*
* @return void
*/
private function migratePostTypeArchiveSettings() {
$supportedSettings = [
'title',
'metadesc',
'noindex'
];
foreach ( aioseo()->helpers->getPublicPostTypes( true, true ) as $postType ) {
foreach ( $this->options as $name => $value ) {
if ( ! preg_match( "#(.*)-ptarchive-$postType$#", (string) $name, $match ) || ! in_array( $match[1], $supportedSettings, true ) ) {
continue;
}
switch ( $match[1] ) {
case 'title':
aioseo()->dynamicOptions->searchAppearance->archives->$postType->title =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, $postType, 'archive' ) );
break;
case 'metadesc':
aioseo()->dynamicOptions->searchAppearance->archives->$postType->metaDescription =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, $postType, 'archive' ) );
break;
case 'noindex':
aioseo()->dynamicOptions->searchAppearance->archives->$postType->show = empty( $value ) ? true : false;
aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->default = empty( $value ) ? true : false;
aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->noindex = empty( $value ) ? false : true;
break;
default:
break;
}
}
}
}
/**
* Migrates the Knowledge Graph settings.
*
* @since 4.0.0
*
* @return void
*/
private function migrateKnowledgeGraphSettings() {
if ( ! empty( $this->options['company_or_person'] ) ) {
aioseo()->options->searchAppearance->global->schema->siteRepresents =
'company' === $this->options['company_or_person'] ? 'organization' : 'person';
}
$settings = [
'company_or_person_user_id' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'person' ] ],
'person_logo' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'personLogo' ] ],
'person_name' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'personName' ] ],
'company_name' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationName' ] ],
'company_logo' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationLogo' ] ],
'org-email' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'email' ] ],
'org-phone' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'phone' ] ],
'org-description' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'organizationDescription' ] ],
'org-founding-date' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'foundingDate' ] ],
];
aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
// Additional Info
// Reset data
aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->reset();
$numberOfEmployees = $this->options['org-number-employees'];
if ( ! empty( $numberOfEmployees ) ) {
list( $num1, $num2 ) = explode( '-', $numberOfEmployees );
if ( $num2 ) {
aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->isRange = true;
aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->from = (int) $num1;
aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->to = (int) $num2;
} else {
aioseo()->options->noConflict()->searchAppearance->global->schema->numberOfEmployees->number = (int) $num1;
}
}
}
/**
* Migrates the RSS content settings.
*
* @since 4.0.0
*
* @return void
*/
private function migrateRssContentSettings() {
if ( isset( $this->options['rssbefore'] ) ) {
aioseo()->options->rssContent->before = esc_html( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['rssbefore'] ) );
}
if ( isset( $this->options['rssafter'] ) ) {
aioseo()->options->rssContent->after = esc_html( aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $this->options['rssafter'] ) );
}
}
/**
* Migrates the Redirect Attachments setting.
*
* @since 4.0.0
*
* @return void
*/
private function migrateRedirectAttachments() {
aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = empty( $this->options['disable-attachment'] ) ? 'disabled' : 'attachment';
}
/**
* Migrates the strip category base option.
*
* @since 4.2.0
*
* @return void
*/
private function migrateStripCategoryBase() {
aioseo()->options->searchAppearance->advanced->removeCategoryBase = empty( $this->options['stripcategorybase'] ) ? false : true;
}
/**
* Migrate the social settings for the homepage.
*
* @since 4.2.4
*
* @return void
*/
private function migrateHomepageSocialSettings() {
if (
empty( $this->options['open_graph_frontpage_title'] ) &&
empty( $this->options['open_graph_frontpage_desc'] ) &&
empty( $this->options['open_graph_frontpage_image'] )
) {
return;
}
$this->hasImportedHomepageSocialSettings = true;
$settings = [
// These settings can also be found in the SocialMeta class, but Yoast recently moved them here.
// We'll still keep them in the other class for backwards compatibility.
'open_graph_frontpage_title' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'title' ] ],
'open_graph_frontpage_desc' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'description' ] ],
'open_graph_frontpage_image' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'image' ] ]
];
aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options, true );
}
} ImportExport/YoastSeo/YoastSeo.php 0000666 00000004174 15113050717 0013257 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\ImportExport;
class YoastSeo extends ImportExport\Importer {
/**
* A list of plugins to look for to import.
*
* @since 4.0.0
*
* @var array
*/
public $plugins = [
[
'name' => 'Yoast SEO',
'version' => '14.0',
'basename' => 'wordpress-seo/wp-seo.php',
'slug' => 'yoast-seo'
],
[
'name' => 'Yoast SEO Premium',
'version' => '14.0',
'basename' => 'wordpress-seo-premium/wp-seo-premium.php',
'slug' => 'yoast-seo-premium'
],
];
/**
* The post action name.
*
* @since 4.0.0
*
* @var string
*/
public $postActionName = 'aioseo_import_post_meta_yoast_seo';
/**
* The user action name.
*
* @since 4.1.4
*
* @var string
*/
public $userActionName = 'aioseo_import_user_meta_yoast_seo';
/**
* UserMeta class instance.
*
* @since 4.2.7
*
* @var UserMeta
*/
private $userMeta = null;
/**
* SearchAppearance class instance.
*
* @since 4.2.7
*
* @var SearchAppearance
*/
public $searchAppearance = null;
/**
* The post action name.
*
* @since 4.0.0
*
* @param ImportExport\ImportExport $importer The main importer class.
*/
public function __construct( $importer ) {
$this->helpers = new Helpers();
$this->postMeta = new PostMeta();
$this->userMeta = new UserMeta();
add_action( $this->postActionName, [ $this->postMeta, 'importPostMeta' ] );
add_action( $this->userActionName, [ $this->userMeta, 'importUserMeta' ] );
$plugins = $this->plugins;
foreach ( $plugins as $key => $plugin ) {
$plugins[ $key ]['class'] = $this;
}
$importer->addPlugins( $plugins );
}
/**
* Imports the settings.
*
* @since 4.0.0
*
* @return void
*/
protected function importSettings() {
new GeneralSettings();
$this->searchAppearance = new SearchAppearance();
// NOTE: The Social Meta settings need to be imported after the Search Appearance ones because some imports depend on what was imported there.
new SocialMeta();
$this->userMeta->scheduleImport();
}
} ImportExport/YoastSeo/PostMeta.php 0000666 00000025720 15113050717 0013245 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\ImportExport;
use AIOSEO\Plugin\Common\Models;
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Imports the post meta from Yoast SEO.
*
* @since 4.0.0
*/
class PostMeta {
/**
* Class constructor.
*
* @since 4.0.0
*/
public function scheduleImport() {
try {
if ( as_next_scheduled_action( aioseo()->importExport->yoastSeo->postActionName ) ) {
return;
}
if ( ! aioseo()->core->cache->get( 'import_post_meta_yoast_seo' ) ) {
aioseo()->core->cache->update( 'import_post_meta_yoast_seo', time(), WEEK_IN_SECONDS );
}
as_schedule_single_action( time(), aioseo()->importExport->yoastSeo->postActionName, [], 'aioseo' );
} catch ( \Exception $e ) {
// Do nothing.
}
}
/**
* Imports the post meta.
*
* @since 4.0.0
*
* @return void
*/
public function importPostMeta() {
$postsPerAction = apply_filters( 'aioseo_import_yoast_seo_posts_per_action', 100 );
$publicPostTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
$timeStarted = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'import_post_meta_yoast_seo' ) );
$posts = aioseo()->core->db
->start( 'posts' . ' as p' )
->select( 'p.ID, p.post_type' )
->leftJoin( 'aioseo_posts as ap', '`p`.`ID` = `ap`.`post_id`' )
->whereRaw( "( p.post_type IN ( '$publicPostTypes' ) )" )
->whereRaw( "( ap.post_id IS NULL OR ap.updated < '$timeStarted' )" )
->orderBy( 'p.ID DESC' )
->limit( $postsPerAction )
->run()
->result();
if ( ! $posts || ! count( $posts ) ) {
aioseo()->core->cache->delete( 'import_post_meta_yoast_seo' );
return;
}
$mappedMeta = [
'_yoast_wpseo_title' => 'title',
'_yoast_wpseo_metadesc' => 'description',
'_yoast_wpseo_canonical' => 'canonical_url',
'_yoast_wpseo_meta-robots-noindex' => 'robots_noindex',
'_yoast_wpseo_meta-robots-nofollow' => 'robots_nofollow',
'_yoast_wpseo_meta-robots-adv' => '',
'_yoast_wpseo_focuskw' => '',
'_yoast_wpseo_focuskeywords' => '',
'_yoast_wpseo_opengraph-title' => 'og_title',
'_yoast_wpseo_opengraph-description' => 'og_description',
'_yoast_wpseo_opengraph-image' => 'og_image_custom_url',
'_yoast_wpseo_twitter-title' => 'twitter_title',
'_yoast_wpseo_twitter-description' => 'twitter_description',
'_yoast_wpseo_twitter-image' => 'twitter_image_custom_url',
'_yoast_wpseo_schema_page_type' => '',
'_yoast_wpseo_schema_article_type' => '',
'_yoast_wpseo_is_cornerstone' => 'pillar_content'
];
foreach ( $posts as $post ) {
$postMeta = aioseo()->core->db
->start( 'postmeta' . ' as pm' )
->select( 'pm.meta_key, pm.meta_value' )
->where( 'pm.post_id', $post->ID )
->whereRaw( "`pm`.`meta_key` LIKE '_yoast_wpseo_%'" )
->run()
->result();
$featuredImage = get_the_post_thumbnail_url( $post->ID );
$meta = [
'post_id' => (int) $post->ID,
'twitter_use_og' => true,
'og_image_type' => $featuredImage ? 'featured' : 'content',
'pillar_content' => 0,
'canonical_url' => '',
'robots_default' => true,
'robots_noarchive' => false,
'robots_nofollow' => false,
'robots_noimageindex' => false,
'robots_noindex' => false,
'robots_noodp' => false,
'robots_nosnippet' => false,
'title' => '',
'description' => '',
'og_title' => '',
'og_description' => '',
'og_image_custom_url' => '',
'twitter_title' => '',
'twitter_description' => '',
'twitter_image_custom_url' => '',
'twitter_image_type' => 'default'
];
if ( ! $postMeta || ! count( $postMeta ) ) {
$aioseoPost = Models\Post::getPost( (int) $post->ID );
$aioseoPost->set( $meta );
$aioseoPost->save();
aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );
continue;
}
$title = '';
foreach ( $postMeta as $record ) {
$name = $record->meta_key;
$value = $record->meta_value;
// Handles primary taxonomy terms.
// We need to handle it separately because it's stored in a different format.
if ( false !== stripos( $name, '_yoast_wpseo_primary_' ) ) {
sscanf( $name, '_yoast_wpseo_primary_%s', $taxonomy );
if ( null === $taxonomy ) {
continue;
}
$options = new \stdClass();
if ( isset( $meta['primary_term'] ) ) {
$options = json_decode( $meta['primary_term'] );
}
$options->$taxonomy = (int) $value;
$meta['primary_term'] = wp_json_encode( $options );
}
if ( ! in_array( $name, array_keys( $mappedMeta ), true ) ) {
continue;
}
switch ( $name ) {
case '_yoast_wpseo_meta-robots-noindex':
case '_yoast_wpseo_meta-robots-nofollow':
if ( (bool) $value ) {
$meta[ $mappedMeta[ $name ] ] = (bool) $value;
$meta['robots_default'] = false;
}
break;
case '_yoast_wpseo_meta-robots-adv':
$supportedValues = [ 'index', 'noarchive', 'noimageindex', 'nosnippet' ];
foreach ( $supportedValues as $val ) {
$meta[ "robots_$val" ] = false;
}
// This is a separated foreach so we can import any and all values.
$values = explode( ',', $value );
if ( $values ) {
$meta['robots_default'] = false;
foreach ( $values as $value ) {
$meta[ "robots_$value" ] = true;
}
}
break;
case '_yoast_wpseo_canonical':
$meta[ $mappedMeta[ $name ] ] = esc_url( $value );
break;
case '_yoast_wpseo_opengraph-image':
$meta['og_image_type'] = 'custom_image';
$meta[ $mappedMeta[ $name ] ] = esc_url( $value );
break;
case '_yoast_wpseo_twitter-image':
$meta['twitter_use_og'] = false;
$meta['twitter_image_type'] = 'custom_image';
$meta[ $mappedMeta[ $name ] ] = esc_url( $value );
break;
case '_yoast_wpseo_schema_page_type':
$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
if ( in_array( $post->post_type, [ 'post', 'page', 'attachment' ], true ) ) {
break;
}
if ( ! in_array( $value, ImportExport\SearchAppearance::$supportedWebPageGraphs, true ) ) {
break;
}
$meta[ $mappedMeta[ $name ] ] = 'WebPage';
$meta['schema_type_options'] = wp_json_encode( [
'webPage' => [
'webPageType' => $value
]
] );
break;
case '_yoast_wpseo_schema_article_type':
$value = aioseo()->helpers->pregReplace( '#\s#', '', $value );
if ( 'none' === lcfirst( $value ) ) {
$meta[ $mappedMeta[ $name ] ] = 'None';
break;
}
if ( in_array( $post->post_type, [ 'page', 'attachment' ], true ) ) {
break;
}
$options = new \stdClass();
if ( isset( $meta['schema_type_options'] ) ) {
$options = json_decode( $meta['schema_type_options'] );
}
$options->article = [ 'articleType' => 'Article' ];
if ( in_array( $value, ImportExport\SearchAppearance::$supportedArticleGraphs, true ) ) {
$options->article = [ 'articleType' => $value ];
} else {
$options->article = [ 'articleType' => 'BlogPosting' ];
}
$meta['schema_type'] = 'Article';
$meta['schema_type_options'] = wp_json_encode( $options );
break;
case '_yoast_wpseo_focuskw':
$focusKeyphrase = [
'focus' => [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $value ) ]
];
// Merge with existing keyphrases if the array key already exists.
if ( ! empty( $meta['keyphrases'] ) ) {
$meta['keyphrases'] = array_merge( $meta['keyphrases'], $focusKeyphrase );
} else {
$meta['keyphrases'] = $focusKeyphrase;
}
break;
case '_yoast_wpseo_focuskeywords':
$keyphrases = [];
if ( ! empty( $meta[ $mappedMeta[ $name ] ] ) ) {
$keyphrases = (array) json_decode( $meta[ $mappedMeta[ $name ] ] );
}
$yoastKeyphrases = json_decode( $value, true );
if ( is_array( $yoastKeyphrases ) ) {
foreach ( $yoastKeyphrases as $yoastKeyphrase ) {
if ( ! empty( $yoastKeyphrase['keyword'] ) ) {
$keyphrase = [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $yoastKeyphrase['keyword'] ) ];
if ( ! isset( $keyphrases['additional'] ) ) {
$keyphrases['additional'] = [];
}
$keyphrases['additional'][] = $keyphrase;
}
}
}
if ( ! empty( $keyphrases ) ) {
// Merge with existing keyphrases if the array key already exists.
if ( ! empty( $meta['keyphrases'] ) ) {
$meta['keyphrases'] = array_merge( $meta['keyphrases'], $keyphrases );
} else {
$meta['keyphrases'] = $keyphrases;
}
}
break;
case '_yoast_wpseo_title':
case '_yoast_wpseo_metadesc':
case '_yoast_wpseo_opengraph-title':
case '_yoast_wpseo_opengraph-description':
case '_yoast_wpseo_twitter-title':
case '_yoast_wpseo_twitter-description':
if ( 'page' === $post->post_type ) {
$value = aioseo()->helpers->pregReplace( '#%%primary_category%%#', '', $value );
$value = aioseo()->helpers->pregReplace( '#%%excerpt%%#', '', $value );
}
if ( '_yoast_wpseo_twitter-title' === $name || '_yoast_wpseo_twitter-description' === $name ) {
$meta['twitter_use_og'] = false;
}
$value = aioseo()->importExport->yoastSeo->helpers->macrosToSmartTags( $value, 'post', $post->post_type );
if ( '_yoast_wpseo_title' === $name ) {
$title = $value;
}
$meta[ $mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
break;
case '_yoast_wpseo_is_cornerstone':
$meta['pillar_content'] = (bool) $value ? 1 : 0;
break;
default:
$meta[ $mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
break;
}
}
// Resetting the `twitter_use_og` option if the user has a custom title and no twitter title.
if ( $meta['twitter_use_og'] && $title && empty( $meta['twitter_title'] ) ) {
$meta['twitter_use_og'] = false;
$meta['twitter_title'] = $title;
}
$aioseoPost = Models\Post::getPost( (int) $post->ID );
$aioseoPost->set( $meta );
$aioseoPost->save();
aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );
// Clear the Overview cache.
aioseo()->postSettings->clearPostTypeOverviewCache( $post->ID );
}
if ( count( $posts ) === $postsPerAction ) {
try {
as_schedule_single_action( time() + 5, aioseo()->importExport->yoastSeo->postActionName, [], 'aioseo' );
} catch ( \Exception $e ) {
// Do nothing.
}
} else {
aioseo()->core->cache->delete( 'import_post_meta_yoast_seo' );
}
}
} ImportExport/YoastSeo/GeneralSettings.php 0000666 00000002233 15113050717 0014601 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the General Settings.
*
* @since 4.0.0
*/
class GeneralSettings {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
$this->options = get_option( 'wpseo' );
if ( empty( $this->options ) ) {
return;
}
$settings = [
'googleverify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'google' ] ],
'msverify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'bing' ] ],
'yandexverify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'yandex' ] ],
'baiduverify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'baidu' ] ],
'enable_xml_sitemap' => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'enable' ] ]
];
aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
}
} ImportExport/YoastSeo/SocialMeta.php 0000666 00000014250 15113050717 0013526 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the Social Meta.
*
* @since 4.0.0
*/
class SocialMeta {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
$this->options = get_option( 'wpseo_social' );
if ( empty( $this->options ) ) {
return;
}
$this->migrateSocialUrls();
$this->migrateFacebookSettings();
$this->migrateTwitterSettings();
$this->migrateFacebookAdminId();
$this->migrateSiteName();
$this->migrateArticleTags();
$this->migrateAdditionalTwitterData();
$settings = [
'pinterestverify' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'pinterest' ] ]
];
aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Migrates the Social URLs.
*
* @since 4.0.0
*
* @return void
*/
private function migrateSocialUrls() {
$settings = [
'facebook_site' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'facebookPageUrl' ] ],
'instagram_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'instagramUrl' ] ],
'linkedin_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'linkedinUrl' ] ],
'myspace_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'myspaceUrl' ] ],
'pinterest_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'pinterestUrl' ] ],
'youtube_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'youtubeUrl' ] ],
'wikipedia_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'wikipediaUrl' ] ],
'wordpress_url' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'wordPressUrl' ] ],
];
if ( ! empty( $this->options['twitter_site'] ) ) {
aioseo()->options->social->profiles->urls->twitterUrl =
'https://x.com/' . aioseo()->helpers->sanitizeOption( $this->options['twitter_site'] );
}
aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Migrates the Facebook settings.
*
* @since 4.0.0
*
* @return void
*/
private function migrateFacebookSettings() {
if ( ! empty( $this->options['og_default_image'] ) ) {
$defaultImage = esc_url( $this->options['og_default_image'] );
aioseo()->options->social->facebook->general->defaultImagePosts = $defaultImage;
aioseo()->options->social->facebook->general->defaultImageSourcePosts = 'default';
aioseo()->options->social->twitter->general->defaultImagePosts = $defaultImage;
aioseo()->options->social->twitter->general->defaultImageSourcePosts = 'default';
}
$settings = [
'opengraph' => [ 'type' => 'boolean', 'newOption' => [ 'social', 'facebook', 'general', 'enable' ] ],
];
if ( ! aioseo()->importExport->yoastSeo->searchAppearance->hasImportedHomepageSocialSettings ) {
// These settings were moved to the Search Appearance tab of Yoast, but we'll leave this here to support older versions.
// However, we want to make sure we import them only if the other ones aren't set.
$settings = array_merge( $settings, [
'og_frontpage_title' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'title' ] ],
'og_frontpage_desc' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'description' ] ],
'og_frontpage_image' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'image' ] ]
] );
}
aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options, true );
// Migrate home page object type.
aioseo()->options->social->facebook->homePage->objectType = 'website';
if ( 'page' === get_option( 'show_on_front' ) ) {
$staticHomePageId = get_option( 'page_on_front' );
// We must check if the ID exists because one might select the static homepage option but not actually set one.
if ( ! $staticHomePageId ) {
return;
}
$aioseoPost = Models\Post::getPost( (int) $staticHomePageId );
$aioseoPost->set( [
'og_object_type' => 'website'
] );
$aioseoPost->save();
}
}
/**
* Migrates the Twitter settings.
*
* @since 4.0.0
*
* @return void
*/
private function migrateTwitterSettings() {
$settings = [
'twitter' => [ 'type' => 'boolean', 'newOption' => [ 'social', 'twitter', 'general', 'enable' ] ],
'twitter_card_type' => [ 'type' => 'string', 'newOption' => [ 'social', 'twitter', 'general', 'defaultCardType' ] ],
];
aioseo()->importExport->yoastSeo->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Migrates the Facebook admin ID.
*
* @since 4.0.0
*
* @return void
*/
private function migrateFacebookAdminId() {
if ( ! empty( $this->options['fbadminapp'] ) ) {
aioseo()->options->social->facebook->advanced->enable = true;
aioseo()->options->social->facebook->advanced->adminId = aioseo()->helpers->sanitizeOption( $this->options['fbadminapp'] );
}
}
/**
* Yoast sets the og:site_name to '#site_title';
*
* @since 4.1.4
*
* @return void
*/
private function migrateSiteName() {
aioseo()->options->social->facebook->general->siteName = '#site_title';
}
/**
* Yoast uses post tags by default, so we need to enable this.
*
* @since 4.1.4
*
* @return void
*/
private function migrateArticleTags() {
aioseo()->options->social->facebook->advanced->enable = true;
aioseo()->options->social->facebook->advanced->generateArticleTags = true;
aioseo()->options->social->facebook->advanced->usePostTagsInTags = true;
aioseo()->options->social->facebook->advanced->useKeywordsInTags = false;
aioseo()->options->social->facebook->advanced->useCategoriesInTags = false;
}
/**
* Enable additional Twitter Data.
*
* @since 4.1.4
*
* @return void
*/
private function migrateAdditionalTwitterData() {
aioseo()->options->social->twitter->general->additionalData = true;
}
} ImportExport/YoastSeo/UserMeta.php 0000666 00000005176 15113050717 0013241 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\YoastSeo;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\ImportExport;
use AIOSEO\Plugin\Common\Models;
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Imports the user meta from Yoast SEO.
*
* @since 4.0.0
*/
class UserMeta {
/**
* Class constructor.
*
* @since 4.0.0
*/
public function scheduleImport() {
aioseo()->actionScheduler->scheduleSingle( aioseo()->importExport->yoastSeo->userActionName, 30 );
if ( ! aioseo()->core->cache->get( 'import_user_meta_yoast_seo' ) ) {
aioseo()->core->cache->update( 'import_user_meta_yoast_seo', 0, WEEK_IN_SECONDS );
}
}
/**
* Imports the post meta.
*
* @since 4.0.0
*
* @return void
*/
public function importUserMeta() {
$usersPerAction = 100;
$offset = aioseo()->core->cache->get( 'import_user_meta_yoast_seo' );
$usersMeta = aioseo()->core->db
->start( aioseo()->core->db->db->usermeta . ' as um', true )
->whereRaw( "um.meta_key IN ('facebook', 'twitter', 'instagram', 'linkedin', 'myspace', 'pinterest', 'soundcloud', 'tumblr', 'wikipedia', 'youtube', 'mastodon', 'bluesky', 'threads')" )
->whereRaw( "um.meta_value != ''" )
->limit( $usersPerAction, $offset )
->run()
->result();
if ( ! $usersMeta || ! count( $usersMeta ) ) {
aioseo()->core->cache->delete( 'import_user_meta_yoast_seo' );
return;
}
$mappedMeta = [
'facebook' => 'aioseo_facebook_page_url',
'twitter' => 'aioseo_twitter_url',
'instagram' => 'aioseo_instagram_url',
'linkedin' => 'aioseo_linkedin_url',
'myspace' => 'aioseo_myspace_url',
'pinterest' => 'aioseo_pinterest_url',
'soundcloud' => 'aioseo_sound_cloud_url',
'tumblr' => 'aioseo_tumblr_url',
'wikipedia' => 'aioseo_wikipedia_url',
'youtube' => 'aioseo_youtube_url',
'bluesky' => 'aioseo_bluesky_url',
'threads' => 'aioseo_threads_url',
'mastodon' => 'aioseo_profiles_additional_urls'
];
foreach ( $usersMeta as $meta ) {
if ( isset( $mappedMeta[ $meta->meta_key ] ) ) {
$value = 'twitter' === $meta->meta_key ? 'https://x.com/' . $meta->meta_value : $meta->meta_value;
update_user_meta( $meta->user_id, $mappedMeta[ $meta->meta_key ], $value );
}
}
if ( count( $usersMeta ) === $usersPerAction ) {
aioseo()->core->cache->update( 'import_user_meta_yoast_seo', 100 + $offset, WEEK_IN_SECONDS );
aioseo()->actionScheduler->scheduleSingle( aioseo()->importExport->yoastSeo->userActionName, 5, [], true );
} else {
aioseo()->core->cache->delete( 'import_user_meta_yoast_seo' );
}
}
} ImportExport/SeoPress/Titles.php 0000666 00000026226 15113050717 0012754 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the Titles Settings.
*
* @since 4.1.4
*/
class Titles {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Class constructor.
*
* @since 4.1.4
*/
public function __construct() {
$this->options = get_option( 'seopress_titles_option_name' );
if ( empty( $this->options ) ) {
return;
}
if (
! empty( $this->options['seopress_titles_archives_author_title'] ) ||
! empty( $this->options['seopress_titles_archives_author_desc'] ) ||
! empty( $this->options['seopress_titles_archives_author_noindex'] )
) {
aioseo()->options->searchAppearance->archives->author->show = true;
}
if (
! empty( $this->options['seopress_titles_archives_date_title'] ) ||
! empty( $this->options['seopress_titles_archives_date_desc'] ) ||
! empty( $this->options['seopress_titles_archives_date_noindex'] )
) {
aioseo()->options->searchAppearance->archives->date->show = true;
}
if (
! empty( $this->options['seopress_titles_archives_search_title'] ) ||
! empty( $this->options['seopress_titles_archives_search_desc'] )
) {
aioseo()->options->searchAppearance->archives->search->show = true;
}
$this->migrateTitleFormats();
$this->migrateDescriptionFormats();
$this->migrateNoIndexFormats();
$this->migratePostTypeSettings();
$this->migrateTaxonomiesSettings();
$this->migrateArchiveSettings();
$this->migrateAdvancedSettings();
$settings = [
'seopress_titles_sep' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'separator' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options, true );
}
/**
* Migrates the title formats.
*
* @since 4.1.4
*
* @return void
*/
private function migrateTitleFormats() {
$settings = [
'seopress_titles_home_site_title' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'siteTitle' ] ],
'seopress_titles_archives_author_title' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'title' ] ],
'seopress_titles_archives_date_title' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'title' ] ],
'seopress_titles_archives_search_title' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'search', 'title' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options, true );
}
/**
* Migrates the description formats.
*
* @since 4.1.4
*
* @return void
*/
private function migrateDescriptionFormats() {
$settings = [
'seopress_titles_home_site_desc' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'metaDescription' ] ],
'seopress_titles_archives_author_desc' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'metaDescription' ] ],
'seopress_titles_archives_date_desc' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'metaDescription' ] ],
'seopress_titles_archives_search_desc' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'archives', 'search', 'metaDescription' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options, true );
}
/**
* Migrates the NoIndex formats.
*
* @since 4.1.4
*
* @return void
*/
private function migrateNoIndexFormats() {
$settings = [
'seopress_titles_archives_author_noindex' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'archives', 'author', 'show' ] ],
'seopress_titles_archives_date_noindex' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'archives', 'date', 'show' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Migrates the post type settings.
*
* @since 4.1.4
*
* @return void
*/
private function migratePostTypeSettings() {
$titles = $this->options['seopress_titles_single_titles'];
if ( empty( $titles ) ) {
return;
}
foreach ( $titles as $postType => $options ) {
if ( ! aioseo()->dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
continue;
}
if ( ! empty( $options['title'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->title =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['title'] ) );
}
if ( ! empty( $options['description'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->metaDescription =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['description'] ) );
}
if ( ! empty( $options['enable'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox = false;
}
if ( ! empty( $options['noindex'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->show = false;
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = false;
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->noindex = true;
}
if ( ! empty( $options['nofollow'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->show = false;
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default = false;
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->nofollow = true;
}
if ( ! empty( $options['date'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showDateInGooglePreview = false;
}
if ( ! empty( $options['thumb_gcs'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->$postType->advanced->showPostThumbnailInSearch = true;
}
}
}
/**
* Migrates the taxonomies settings.
*
* @since 4.1.4
*
* @return void
*/
private function migrateTaxonomiesSettings() {
$titles = ! empty( $this->options['seopress_titles_tax_titles'] ) ? $this->options['seopress_titles_tax_titles'] : '';
if ( empty( $titles ) ) {
return;
}
foreach ( $titles as $taxonomy => $options ) {
if ( ! aioseo()->dynamicOptions->searchAppearance->taxonomies->has( $taxonomy ) ) {
continue;
}
if ( ! empty( $options['title'] ) ) {
aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->title =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['title'] ) );
}
if ( ! empty( $options['description'] ) ) {
aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->metaDescription =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['description'] ) );
}
if ( ! empty( $options['enable'] ) ) {
aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->showMetaBox = false;
}
if ( ! empty( $options['noindex'] ) ) {
aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->show = false;
aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->default = false;
aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->noindex = true;
}
if ( ! empty( $options['nofollow'] ) ) {
aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->show = false;
aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->default = false;
aioseo()->dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->nofollow = true;
}
}
}
/**
* Migrates the archives settings.
*
* @since 4.1.4
*
* @return void
*/
private function migrateArchiveSettings() {
$titles = $this->options['seopress_titles_archive_titles'];
if ( empty( $titles ) ) {
return;
}
foreach ( $titles as $archive => $options ) {
if ( ! aioseo()->dynamicOptions->searchAppearance->archives->has( $archive ) ) {
continue;
}
if ( ! empty( $options['title'] ) ) {
aioseo()->dynamicOptions->searchAppearance->archives->$archive->title =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['title'] ) );
}
if ( ! empty( $options['description'] ) ) {
aioseo()->dynamicOptions->searchAppearance->archives->$archive->metaDescription =
aioseo()->helpers->sanitizeOption( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $options['description'] ) );
}
if ( ! empty( $options['noindex'] ) ) {
aioseo()->dynamicOptions->searchAppearance->archives->$archive->show = false;
aioseo()->dynamicOptions->searchAppearance->archives->$archive->advanced->robotsMeta->default = false;
aioseo()->dynamicOptions->searchAppearance->archives->$archive->advanced->robotsMeta->noindex = true;
}
if ( ! empty( $options['nofollow'] ) ) {
aioseo()->dynamicOptions->searchAppearance->archives->$archive->show = false;
aioseo()->dynamicOptions->searchAppearance->archives->$archive->advanced->robotsMeta->default = false;
aioseo()->dynamicOptions->searchAppearance->archives->$archive->advanced->robotsMeta->nofollow = true;
}
}
}
/**
* Migrates the advanced settings.
*
* @since 4.1.4
*
* @return void
*/
private function migrateAdvancedSettings() {
if (
! empty( $this->options['seopress_titles_noindex'] ) || ! empty( $this->options['seopress_titles_nofollow'] ) || ! empty( $this->options['seopress_titles_noodp'] ) ||
! empty( $this->options['seopress_titles_noimageindex'] ) || ! empty( $this->options['seopress_titles_noarchive'] ) ||
! empty( $this->options['seopress_titles_nosnippet'] ) || ! empty( $this->options['seopress_titles_paged_noindex'] )
) {
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default = false;
}
$settings = [
'seopress_titles_noindex' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noindex' ] ],
'seopress_titles_nofollow' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'nofollow' ] ],
'seopress_titles_noodp' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noodp' ] ],
'seopress_titles_noimageindex' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noimageindex' ] ],
'seopress_titles_noarchive' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noarchive' ] ],
'seopress_titles_nosnippet' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'nosnippet' ] ],
'seopress_titles_paged_noindex' => [ 'type' => 'boolean', 'newOption' => [ 'searchAppearance', 'advanced', 'globalRobotsMeta', 'noindexPaginated' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
}
} ImportExport/SeoPress/PostMeta.php 0000666 00000015547 15113050717 0013250 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Imports the post meta from SEOPress.
*
* @since 4.1.4
*/
class PostMeta {
/**
* The mapped meta
*
* @since 4.1.4
*
* @var array
*/
private $mappedMeta = [
'_seopress_analysis_target_kw' => '',
'_seopress_robots_archive' => 'robots_noarchive',
'_seopress_robots_canonical' => 'canonical_url',
'_seopress_robots_follow' => 'robots_nofollow',
'_seopress_robots_imageindex' => 'robots_noimageindex',
'_seopress_robots_index' => 'robots_noindex',
'_seopress_robots_odp' => 'robots_noodp',
'_seopress_robots_snippet' => 'robots_nosnippet',
'_seopress_social_twitter_desc' => 'twitter_description',
'_seopress_social_twitter_img' => 'twitter_image_custom_url',
'_seopress_social_twitter_title' => 'twitter_title',
'_seopress_social_fb_desc' => 'og_description',
'_seopress_social_fb_img' => 'og_image_custom_url',
'_seopress_social_fb_title' => 'og_title',
'_seopress_titles_desc' => 'description',
'_seopress_titles_title' => 'title',
'_seopress_robots_primary_cat' => 'primary_term'
];
/**
* Class constructor.
*
* @since 4.1.4
*/
public function scheduleImport() {
if ( aioseo()->actionScheduler->scheduleSingle( aioseo()->importExport->seoPress->postActionName, 0 ) ) {
if ( ! aioseo()->core->cache->get( 'import_post_meta_seopress' ) ) {
aioseo()->core->cache->update( 'import_post_meta_seopress', time(), WEEK_IN_SECONDS );
}
}
}
/**
* Imports the post meta.
*
* @since 4.1.4
*
* @return void
*/
public function importPostMeta() {
$postsPerAction = apply_filters( 'aioseo_import_seopress_posts_per_action', 100 );
$publicPostTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
$timeStarted = gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'import_post_meta_seopress' ) );
$posts = aioseo()->core->db
->start( 'posts as p' )
->select( 'p.ID, p.post_type' )
->join( 'postmeta as pm', '`p`.`ID` = `pm`.`post_id`' )
->leftJoin( 'aioseo_posts as ap', '`p`.`ID` = `ap`.`post_id`' )
->whereRaw( "pm.meta_key LIKE '_seopress_%'" )
->whereRaw( "( p.post_type IN ( '$publicPostTypes' ) )" )
->whereRaw( "( ap.post_id IS NULL OR ap.updated < '$timeStarted' )" )
->groupBy( 'p.ID' )
->orderBy( 'p.ID DESC' )
->limit( $postsPerAction )
->run()
->result();
if ( ! $posts || ! count( $posts ) ) {
aioseo()->core->cache->delete( 'import_post_meta_seopress' );
return;
}
foreach ( $posts as $post ) {
$postMeta = aioseo()->core->db
->start( 'postmeta' . ' as pm' )
->select( 'pm.meta_key, pm.meta_value' )
->where( 'pm.post_id', $post->ID )
->whereRaw( "`pm`.`meta_key` LIKE '_seopress_%'" )
->run()
->result();
$meta = array_merge( [
'post_id' => (int) $post->ID,
], $this->getMetaData( $postMeta, $post->ID ) );
if ( ! $postMeta || ! count( $postMeta ) ) {
$aioseoPost = Models\Post::getPost( (int) $post->ID );
$aioseoPost->set( $meta );
$aioseoPost->save();
aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );
continue;
}
$aioseoPost = Models\Post::getPost( (int) $post->ID );
$aioseoPost->set( $meta );
$aioseoPost->save();
aioseo()->migration->meta->migrateAdditionalPostMeta( $post->ID );
// Clear the Overview cache.
aioseo()->postSettings->clearPostTypeOverviewCache( $post->ID );
}
if ( count( $posts ) === $postsPerAction ) {
aioseo()->actionScheduler->scheduleSingle( aioseo()->importExport->seoPress->postActionName, 5, [], true );
} else {
aioseo()->core->cache->delete( 'import_post_meta_seopress' );
}
}
/**
* Get the meta data by post meta.
*
* @since 4.1.4
*
* @param object $postMeta The post meta from database.
* @return array The meta data.
*/
public function getMetaData( $postMeta, $postId ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$meta = [
'robots_default' => true,
'robots_noarchive' => false,
'canonical_url' => '',
'robots_nofollow' => false,
'robots_noimageindex' => false,
'robots_noindex' => false,
'robots_noodp' => false,
'robots_nosnippet' => false,
'twitter_use_og' => aioseo()->options->social->twitter->general->useOgData,
'twitter_title' => '',
'twitter_description' => ''
];
foreach ( $postMeta as $record ) {
$name = $record->meta_key;
$value = $record->meta_value;
if ( ! in_array( $name, array_keys( $this->mappedMeta ), true ) ) {
continue;
}
switch ( $name ) {
case '_seopress_analysis_target_kw':
$keyphrases = array_map( 'trim', explode( ',', $value ) );
$keyphraseArray = [
'focus' => [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $keyphrases[0] ) ],
'additional' => []
];
unset( $keyphrases[0] );
foreach ( $keyphrases as $keyphrase ) {
$keyphraseArray['additional'][] = [ 'keyphrase' => aioseo()->helpers->sanitizeOption( $keyphrase ) ];
}
$meta['keyphrases'] = $keyphraseArray;
break;
case '_seopress_robots_snippet':
case '_seopress_robots_archive':
case '_seopress_robots_imageindex':
case '_seopress_robots_odp':
case '_seopress_robots_follow':
case '_seopress_robots_index':
if ( 'yes' === $value ) {
$meta['robots_default'] = false;
$meta[ $this->mappedMeta[ $name ] ] = true;
}
break;
case '_seopress_social_twitter_img':
$meta['twitter_use_og'] = false;
$meta['twitter_image_type'] = 'custom_image';
$meta[ $this->mappedMeta[ $name ] ] = esc_url( $value );
break;
case '_seopress_social_twitter_desc':
case '_seopress_social_twitter_title':
$meta['twitter_use_og'] = false;
$meta[ $this->mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
break;
case '_seopress_social_fb_img':
$meta['og_image_type'] = 'custom_image';
$meta[ $this->mappedMeta[ $name ] ] = esc_url( $value );
break;
case '_seopress_robots_primary_cat':
$taxonomy = 'category';
$options = new \stdClass();
$options->$taxonomy = (int) $value;
$meta[ $this->mappedMeta[ $name ] ] = wp_json_encode( $options );
break;
case '_seopress_titles_title':
case '_seopress_titles_desc':
$value = aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $value );
default:
$meta[ $this->mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
break;
}
}
return $meta;
}
} ImportExport/SeoPress/Helpers.php 0000666 00000007264 15113050717 0013113 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\ImportExport;
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Contains helper methods for the import from SEOPress.
*
* @since 4.1.4
*/
class Helpers extends ImportExport\Helpers {
/**
* Converts the macros from SEOPress to our own smart tags.
*
* @since 4.1.4
*
* @param string $string The string with macros.
* @param string $postType The post type.
* @return string The string with smart tags.
*/
public function macrosToSmartTags( $string, $postType = null ) {
$macros = $this->getMacros( $postType );
foreach ( $macros as $macro => $tag ) {
$string = aioseo()->helpers->pregReplace( "#$macro(?![a-zA-Z0-9_])#im", $tag, $string );
}
return trim( $string );
}
/**
* Returns the macro mappings.
*
* @since 4.1.4
*
* @param string $postType The post type.
* @param string $pageType The page type.
* @return array $macros The macros.
*/
protected function getMacros( $postType = null, $pageType = null ) {
$macros = [
'%%sep%%' => '#separator_sa',
'%%sitetitle%%' => '#site_title',
'%%sitename%%' => '#site_title',
'%%tagline%%' => '#tagline',
'%%sitedesc%%' => '#tagline',
'%%title%%' => '#site_title',
'%%post_title%%' => '#post_title',
'%%post_excerpt%%' => '#post_excerpt',
'%%excerpt%%' => '#post_excerpt',
'%%post_content%%' => '#post_content',
'%%post_url%%' => '#permalink',
'%%post_date%%' => '#post_date',
'%%post_permalink%%' => '#permalink',
'%%date%%' => '#post_date',
'%%post_author%%' => '#author_name',
'%%post_category%%' => '#categories',
'%%_category_title%%' => '#taxonomy_title',
'%%_category_description%%' => '#taxonomy_description',
'%%tag_title%%' => '#taxonomy_title',
'%%tag_description%%' => '#taxonomy_description',
'%%term_title%%' => '#taxonomy_title',
'%%term_description%%' => '#taxonomy_description',
'%%search_keywords%%' => '#search_term',
'%%current_pagination%%' => '#page_number',
'%%page%%' => '#page_number',
'%%archive_title%%' => '#archive_title',
'%%archive_date%%' => '#archive_date',
'%%wc_single_price%%' => '#woocommerce_price',
'%%wc_sku%%' => '#woocommerce_sku',
'%%currentday%%' => '#current_day',
'%%currentmonth%%' => '#current_month',
'%%currentmonth_short%%' => '#current_month',
'%%currentyear%%' => '#current_year',
'%%currentdate%%' => '#current_date',
'%%author_first_name%%' => '#author_first_name',
'%%author_last_name%%' => '#author_last_name',
'%%author_website%%' => '#author_link',
'%%author_nickname%%' => '#author_first_name',
'%%author_bio%%' => '#author_bio',
'%%currentmonth_num%%' => '#current_month',
];
if ( $postType ) {
$postType = get_post_type_object( $postType );
if ( ! empty( $postType ) ) {
$macros += [
'%%cpt_plural%%' => $postType->labels->name,
];
}
}
switch ( $pageType ) {
case 'archive':
$macros['%%title%%'] = '#archive_title';
break;
case 'term':
$macros['%%title%%'] = '#taxonomy_title';
break;
default:
$macros['%%title%%'] = '#post_title';
break;
}
// Strip all other tags.
$macros['%%[^%]*%%'] = '';
return $macros;
}
} ImportExport/SeoPress/SocialMeta.php 0000666 00000012322 15113050717 0013521 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the Social Meta Settings.
*
* @since 4.1.4
*/
class SocialMeta {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Class constructor.
*
* @since 4.1.4
*/
public function __construct() {
$this->options = get_option( 'seopress_social_option_name' );
if ( empty( $this->options ) ) {
return;
}
$this->migrateSocialUrls();
$this->migrateKnowledge();
$this->migrateFacebookSettings();
$this->migrateTwitterSettings();
}
/**
* Migrates Basic Social Profiles URLs.
*
* @since 4.1.4
*
* @return void
*/
private function migrateSocialUrls() {
$settings = [
'seopress_social_accounts_facebook' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'facebookPageUrl' ] ],
'seopress_social_accounts_twitter' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'twitterUrl' ] ],
'seopress_social_accounts_pinterest' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'pinterestUrl' ] ],
'seopress_social_accounts_instagram' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'instagramUrl' ] ],
'seopress_social_accounts_youtube' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'youtubeUrl' ] ],
'seopress_social_accounts_linkedin' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'linkedinUrl' ] ],
'seopress_social_accounts_myspace' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'myspaceUrl' ] ],
'seopress_social_accounts_soundcloud' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'soundCloudUrl' ] ],
'seopress_social_accounts_tumblr' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'tumblrUrl' ] ],
'seopress_social_accounts_wordpress' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'wordPressUrl' ] ],
'seopress_social_accounts_bluesky' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'blueskyUrl' ] ],
'seopress_social_accounts_threads' => [ 'type' => 'string', 'newOption' => [ 'social', 'profiles', 'urls', 'threadsUrl' ] ]
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Migrates Knowledge Graph data.
*
* @since 4.1.4
*
* @return void
*/
private function migrateKnowledge() {
$type = 'organization';
if ( ! empty( $this->options['seopress_social_knowledge_type'] ) ) {
$type = strtolower( $this->options['seopress_social_knowledge_type'] );
if ( 'person' === $type ) {
aioseo()->options->searchAppearance->global->schema->person = 'manual';
}
}
aioseo()->options->searchAppearance->global->schema->siteRepresents = $type;
$settings = [
'seopress_social_knowledge_img' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', $type . 'Logo' ] ],
'seopress_social_knowledge_name' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', $type . 'Name' ] ],
'seopress_social_knowledge_phone' => [ 'type' => 'string', 'newOption' => [ 'searchAppearance', 'global', 'schema', 'phone' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Migrates the Facebook settings.
*
* @since 4.1.4
*
* @return void
*/
private function migrateFacebookSettings() {
if ( ! empty( $this->options['seopress_social_facebook_admin_id'] ) || ! empty( $this->options['seopress_social_facebook_app_id'] ) ) {
aioseo()->options->social->facebook->advanced->enable = true;
}
$settings = [
'seopress_social_facebook_og' => [ 'type' => 'boolean', 'newOption' => [ 'social', 'facebook', 'general', 'enable' ] ],
'seopress_social_facebook_img' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'homePage', 'image' ] ],
'seopress_social_facebook_admin_id' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'advanced', 'adminId' ] ],
'seopress_social_facebook_app_id' => [ 'type' => 'string', 'newOption' => [ 'social', 'facebook', 'advanced', 'appId' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Migrates the Twitter settings.
*
* @since 4.1.4
*
* @return void
*/
private function migrateTwitterSettings() {
if ( ! empty( $this->options['seopress_social_twitter_card_img_size'] ) ) {
$twitterCard = ( 'large' === $this->options['seopress_social_twitter_card_img_size'] ) ? 'summary-card' : 'summary';
aioseo()->options->social->twitter->general->defaultCardType = $twitterCard;
}
$settings = [
'seopress_social_twitter_card' => [ 'type' => 'boolean', 'newOption' => [ 'social', 'twitter', 'general', 'enable' ] ],
'seopress_social_twitter_card_img' => [ 'type' => 'string', 'newOption' => [ 'social', 'twitter', 'homePage', 'image' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
}
} ImportExport/SeoPress/Breadcrumbs.php 0000666 00000003403 15113050717 0013731 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the Breadcrumb settings.
*
* @since 4.1.4
*/
class Breadcrumbs {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Class constructor.
*
* @since 4.1.4
*/
public function __construct() {
$this->options = get_option( 'seopress_pro_option_name' );
if ( empty( $this->options ) ) {
return;
}
$this->migrate();
}
/**
* Migrates the Breadcrumbs settings.
*
* @since 4.1.4
*
* @return void
*/
private function migrate() {
if ( ! empty( $this->options['seopress_breadcrumbs_i18n_search'] ) ) {
aioseo()->options->breadcrumbs->searchResultFormat = sprintf( '%1$s #breadcrumb_archive_post_type_name', $this->options['seopress_breadcrumbs_i18n_search'] );
}
if ( ! empty( $this->options['seopress_breadcrumbs_remove_blog_page'] ) ) {
aioseo()->options->breadcrumbs->showBlogHome = false;
}
$settings = [
'seopress_breadcrumbs_enable' => [ 'type' => 'boolean', 'newOption' => [ 'breadcrumbs', 'enable' ] ],
'seopress_breadcrumbs_separator' => [ 'type' => 'string', 'newOption' => [ 'breadcrumbs', 'separator' ] ],
'seopress_breadcrumbs_i18n_home' => [ 'type' => 'string', 'newOption' => [ 'breadcrumbs', 'homepageLabel' ] ],
'seopress_breadcrumbs_i18n_here' => [ 'type' => 'string', 'newOption' => [ 'breadcrumbs', 'breadcrumbPrefix' ] ],
'seopress_breadcrumbs_i18n_404' => [ 'type' => 'string', 'newOption' => [ 'breadcrumbs', 'errorFormat404' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
}
} ImportExport/SeoPress/Sitemap.php 0000666 00000003600 15113050717 0013101 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the Sitemap Settings.
*
* @since 4.1.4
*/
class Sitemap {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Class constructor.
*
* @since 4.1.4
*/
public function __construct() {
$this->options = get_option( 'seopress_xml_sitemap_option_name' );
if ( empty( $this->options ) ) {
return;
}
$this->migratePostTypesInclude();
$this->migrateTaxonomiesInclude();
$settings = [
'seopress_xml_sitemap_general_enable' => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'enable' ] ],
'seopress_xml_sitemap_author_enable' => [ 'type' => 'boolean', 'newOption' => [ 'sitemap', 'general', 'author' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Migrates the post types to include in sitemap settings.
*
* @since 4.1.4
*
* @return void
*/
public function migratePostTypesInclude() {
$postTypesMigrate = $this->options['seopress_xml_sitemap_post_types_list'];
$postTypesInclude = [];
foreach ( $postTypesMigrate as $postType => $options ) {
$postTypesInclude[] = $postType;
}
aioseo()->options->sitemap->general->postTypes->included = $postTypesInclude;
}
/**
* Migrates the taxonomies to include in sitemap settings.
*
* @since 4.1.4
*
* @return void
*/
public function migrateTaxonomiesInclude() {
$taxonomiesMigrate = $this->options['seopress_xml_sitemap_taxonomies_list'];
$taxonomiesInclude = [];
foreach ( $taxonomiesMigrate as $taxonomy => $options ) {
$taxonomiesInclude[] = $taxonomy;
}
aioseo()->options->sitemap->general->taxonomies->included = $taxonomiesInclude;
}
} ImportExport/SeoPress/Analytics.php 0000666 00000001527 15113050717 0013434 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the Analytics Settings.
*
* @since 4.1.4
*/
class Analytics {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Class constructor.
*
* @since 4.1.4
*/
public function __construct() {
$this->options = get_option( 'seopress_google_analytics_option_name' );
if ( empty( $this->options ) ) {
return;
}
$settings = [
'seopress_google_analytics_other_tracking' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'miscellaneousVerification' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
}
} ImportExport/SeoPress/Rss.php 0000666 00000002242 15113050717 0012247 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the RSS settings.
*
* @since 4.1.4
*/
class Rss {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Class constructor.
*
* @since 4.1.4
*/
public function __construct() {
$this->options = get_option( 'seopress_pro_option_name' );
if ( empty( $this->options ) ) {
return;
}
$this->migrateRss();
}
/**
* Migrates the RSS settings.
*
* @since 4.1.4
*
* @return void
*/
public function migrateRss() {
if ( ! empty( $this->options['seopress_rss_before_html'] ) ) {
aioseo()->options->rssContent->before = esc_html( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $this->options['seopress_rss_before_html'] ) );
}
if ( ! empty( $this->options['seopress_rss_after_html'] ) ) {
aioseo()->options->rssContent->after = esc_html( aioseo()->importExport->seoPress->helpers->macrosToSmartTags( $this->options['seopress_rss_after_html'] ) );
}
}
} ImportExport/SeoPress/GeneralSettings.php 0000666 00000010307 15113050717 0014577 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the General Settings.
*
* @since 4.1.4
*/
class GeneralSettings {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* List of our access control roles.
*
* @since 4.2.7
*
* @var array
*/
private $roles = [];
/**
* Class constructor.
*
* @since 4.1.4
*/
public function __construct() {
$this->options = get_option( 'seopress_advanced_option_name' );
if ( empty( $this->options ) ) {
return;
}
$this->roles = aioseo()->access->getRoles();
$this->migrateBlockMetaboxRoles();
$this->migrateBlockContentAnalysisRoles();
$this->migrateAttachmentRedirects();
$settings = [
'seopress_advanced_advanced_google' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'google' ] ],
'seopress_advanced_advanced_bing' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'bing' ] ],
'seopress_advanced_advanced_pinterest' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'pinterest' ] ],
'seopress_advanced_advanced_yandex' => [ 'type' => 'string', 'newOption' => [ 'webmasterTools', 'yandex' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Migrates Block AIOSEO metabox setting.
*
* @since 4.1.4
*
* @return void
*/
private function migrateBlockMetaboxRoles() {
$seoPressRoles = ! empty( $this->options['seopress_advanced_security_metaboxe_role'] ) ? $this->options['seopress_advanced_security_metaboxe_role'] : '';
if ( empty( $seoPressRoles ) ) {
return;
}
$roleSettings = [ 'useDefault', 'pageAnalysis', 'pageGeneralSettings', 'pageSocialSettings', 'pageSchemaSettings', 'pageAdvancedSettings' ];
foreach ( $seoPressRoles as $wpRole => $value ) {
$role = $this->roles[ $wpRole ];
if ( empty( $role ) || aioseo()->access->isAdmin( $role ) ) {
continue;
}
if ( aioseo()->options->accessControl->has( $role ) ) {
foreach ( $roleSettings as $setting ) {
aioseo()->options->accessControl->$role->$setting = false;
}
} elseif ( aioseo()->dynamicOptions->accessControl->has( $role ) ) {
foreach ( $roleSettings as $setting ) {
aioseo()->dynamicOptions->accessControl->$role->$setting = false;
}
}
}
}
/**
* Migrates Block Content analysis metabox setting.
*
* @since 4.1.4
*
* @return void
*/
private function migrateBlockContentAnalysisRoles() {
$seoPressRoles = ! empty( $this->options['seopress_advanced_security_metaboxe_ca_role'] ) ? $this->options['seopress_advanced_security_metaboxe_ca_role'] : '';
if ( empty( $seoPressRoles ) ) {
return;
}
$roleSettings = [ 'useDefault', 'pageAnalysis' ];
foreach ( $seoPressRoles as $wpRole => $value ) {
$role = $this->roles[ $wpRole ];
if ( empty( $role ) || aioseo()->access->isAdmin( $role ) ) {
continue;
}
if ( aioseo()->options->accessControl->has( $role ) ) {
foreach ( $roleSettings as $setting ) {
aioseo()->options->accessControl->$role->$setting = false;
}
} elseif ( aioseo()->dynamicOptions->accessControl->has( $role ) ) {
foreach ( $roleSettings as $setting ) {
aioseo()->dynamicOptions->accessControl->$role->$setting = false;
}
}
}
}
/**
* Migrates redirect attachment pages settings.
*
* @since 4.1.4
*
* @return void
*/
private function migrateAttachmentRedirects() {
if ( ! empty( $this->options['seopress_advanced_advanced_attachments'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'attachment_parent';
}
if ( ! empty( $this->options['seopress_advanced_advanced_attachments_file'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'attachment';
}
if ( empty( $this->options['seopress_advanced_advanced_attachments'] ) && empty( $this->options['seopress_advanced_advanced_attachments_file'] ) ) {
aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls = 'disabled';
}
}
} ImportExport/SeoPress/SeoPress.php 0000666 00000002761 15113050717 0013251 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\ImportExport;
class SeoPress extends ImportExport\Importer {
/**
* A list of plugins to look for to import.
*
* @since 4.1.4
*
* @var array
*/
public $plugins = [
[
'name' => 'SEOPress',
'version' => '4.0',
'basename' => 'wp-seopress/seopress.php',
'slug' => 'seopress'
],
[
'name' => 'SEOPress PRO',
'version' => '4.0',
'basename' => 'wp-seopress-pro/seopress-pro.php',
'slug' => 'seopress-pro'
],
];
/**
* The post action name.
*
* @since 4.1.4
*
* @var string
*/
public $postActionName = 'aioseo_import_post_meta_seopress';
/**
* The post action name.
*
* @since 4.1.4
*
* @param ImportExport\ImportExport $importer The main importer class.
*/
public function __construct( $importer ) {
$this->helpers = new Helpers();
$this->postMeta = new PostMeta();
add_action( $this->postActionName, [ $this->postMeta, 'importPostMeta' ] );
$plugins = $this->plugins;
foreach ( $plugins as $key => $plugin ) {
$plugins[ $key ]['class'] = $this;
}
$importer->addPlugins( $plugins );
}
/**
* Imports the settings.
*
* @since 4.1.4
*
* @return void
*/
protected function importSettings() {
new GeneralSettings();
new Analytics();
new SocialMeta();
new Titles();
new Sitemap();
new RobotsTxt();
new Rss();
new Breadcrumbs();
}
} ImportExport/SeoPress/RobotsTxt.php 0000666 00000002357 15113050717 0013457 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport\SeoPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
/**
* Migrates the robots.txt settings.
*
* @since 4.1.4
*/
class RobotsTxt {
/**
* List of options.
*
* @since 4.2.7
*
* @var array
*/
private $options = [];
/**
* Class constructor.
*
* @since 4.1.4
*/
public function __construct() {
$this->options = get_option( 'seopress_pro_option_name', [] );
if ( empty( $this->options ) ) {
return;
}
$this->migrateRobotsTxt();
$settings = [
'seopress_robots_enable' => [ 'type' => 'boolean', 'newOption' => [ 'tools', 'robots', 'enable' ] ],
];
aioseo()->importExport->seoPress->helpers->mapOldToNew( $settings, $this->options );
}
/**
* Migrates the robots.txt.
*
* @since 4.1.4
*
* @return void
*/
public function migrateRobotsTxt() {
$lines = ! empty( $this->options['seopress_robots_file'] ) ? (string) $this->options['seopress_robots_file'] : '';
if ( $lines ) {
$allRules = aioseo()->robotsTxt->extractRules( $lines );
aioseo()->options->tools->robots->rules = aioseo()->robotsTxt->prepareRobotsTxt( $allRules );
}
}
} ImportExport/Helpers.php 0000666 00000004430 15113050717 0011340 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\ImportExport;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Contains helper methods for the import from other plugins.
*
* @since 4.0.0
*/
abstract class Helpers {
/**
* Converts macros to smart tags.
*
* @since 4.1.3
*
* @param string $value The string with macros.
* @return string The string with macros converted.
*/
abstract public function macrosToSmartTags( $value );
/**
* Maps a list of old settings from V3 to their counterparts in V4.
*
* @since 4.0.0
*
* @param array $mappings The old settings, mapped to their new settings.
* @param array $group The old settings group.
* @param bool $convertMacros Whether to convert the old V3 macros to V4 smart tags.
* @return void
*/
public function mapOldToNew( $mappings, $group, $convertMacros = false ) {
if (
! is_array( $mappings ) ||
! is_array( $group ) ||
! count( $mappings ) ||
! count( $group )
) {
return;
}
$mainOptions = aioseo()->options->noConflict();
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
foreach ( $mappings as $name => $values ) {
if ( ! isset( $group[ $name ] ) ) {
continue;
}
$error = false;
$options = ! empty( $values['dynamic'] ) ? $dynamicOptions : $mainOptions;
$lastOption = '';
for ( $i = 0; $i < count( $values['newOption'] ); $i++ ) {
$lastOption = $values['newOption'][ $i ];
if ( ! $options->has( $lastOption, false ) ) {
$error = true;
break;
}
if ( count( $values['newOption'] ) - 1 !== $i ) {
$options = $options->$lastOption;
}
}
if ( $error ) {
continue;
}
switch ( $values['type'] ) {
case 'boolean':
if ( ! empty( $group[ $name ] ) ) {
$options->$lastOption = true;
break;
}
$options->$lastOption = false;
break;
case 'integer':
case 'float':
$value = aioseo()->helpers->sanitizeOption( $group[ $name ] );
if ( $value ) {
$options->$lastOption = $value;
}
break;
default:
$value = $group[ $name ];
if ( $convertMacros ) {
$value = $this->macrosToSmartTags( $value );
}
$options->$lastOption = aioseo()->helpers->sanitizeOption( $value );
break;
}
}
}
} Standalone/UserProfileTab.php 0000666 00000012537 15113050717 0012247 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Pro\Standalone as ProStandalone;
/**
* Registers the standalone components.
*
* @since 4.2.2
*/
class UserProfileTab {
/**
* Class constructor.
*
* @since 4.2.2
*/
public function __construct() {
if ( ! is_admin() ) {
return;
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScript' ] );
add_action( 'profile_update', [ $this, 'updateUserSocialProfiles' ] );
}
/**
* Enqueues the script.
*
* @since 4.2.2
*
* @return void
*/
public function enqueueScript() {
if ( apply_filters( 'aioseo_user_profile_tab_disable', false ) ) {
return;
}
$screen = aioseo()->helpers->getCurrentScreen();
if ( empty( $screen->id ) ) {
return;
}
if ( ! in_array( $screen->id, [ 'user-edit', 'profile' ], true ) ) {
if ( 'follow-up_page_followup-emails-reports' === $screen->id ) {
aioseo()->core->assets->load( 'src/vue/standalone/user-profile-tab/follow-up-emails-nav-bar.js' );
}
return;
}
global $user_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
if ( ! intval( $user_id ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
return;
}
aioseo()->core->assets->load( 'src/vue/standalone/user-profile-tab/main.js', [], $this->getVueData() );
// Load script again so we can add extra data to localize the strings.
aioseo()->core->assets->load( 'src/vue/standalone/user-profile-tab/main.js', [], [
'translations' => aioseo()->helpers->getJedLocaleData( 'aioseo-eeat' )
], 'eeat' );
}
/**
* Returns the data Vue requires.
*
* @since 4.2.2
*
* @return array
*/
public function getVueData() {
global $user_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
$socialProfiles = $this->getSocialProfiles();
foreach ( $socialProfiles as $platformKey => $v ) {
$metaName = 'aioseo_' . aioseo()->helpers->toSnakeCase( $platformKey );
$socialProfiles[ $platformKey ] = get_user_meta( $user_id, $metaName, true ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
}
$sameUsername = get_user_meta( $user_id, 'aioseo_profiles_same_username', true ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
if ( empty( $sameUsername ) ) {
$sameUsername = [
'enable' => false,
'username' => '',
'included' => [ 'facebookPageUrl', 'twitterUrl', 'tiktokUrl', 'pinterestUrl', 'instagramUrl', 'youtubeUrl', 'linkedinUrl' ] // Same as in Options.php.
];
}
$additionalurls = get_user_meta( $user_id, 'aioseo_profiles_additional_urls', true ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
$extraVueData = [
'userProfile' => [
'userData' => get_userdata( $user_id )->data, // phpcs:ignore Squiz.NamingConventions.ValidVariableName
'profiles' => [
'sameUsername' => $sameUsername,
'urls' => $socialProfiles,
'additionalUrls' => $additionalurls
],
'isWooCommerceFollowupEmailsActive' => aioseo()->helpers->isWooCommerceFollowupEmailsActive()
]
];
$vueData = aioseo()->helpers->getVueData();
$vueData = array_merge( $vueData, $extraVueData );
return $vueData;
}
/**
* Updates the user social profile URLs when a user's profile is updated.
*
* @since 4.2.2
*
* @param int $userId The user ID.
* @return void
*/
public function updateUserSocialProfiles( $userId ) {
if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $userId ) ) {
return;
}
if ( empty( $_POST['aioseo-user-social-profiles'] ) ) {
return;
}
$data = json_decode( sanitize_text_field( wp_unslash( $_POST['aioseo-user-social-profiles'] ) ), true );
if ( empty( $data ) ) {
return;
}
$sanitizedIncluded = [];
foreach ( $data['sameUsername']['included'] as $platformKey ) {
$sanitizedIncluded[] = sanitize_text_field( $platformKey );
}
$sanitizedSameUsernameData = [
'enable' => (bool) $data['sameUsername']['enable'],
'username' => sanitize_text_field( $data['sameUsername']['username'] ),
'included' => array_filter( $sanitizedIncluded )
];
update_user_meta( $userId, 'aioseo_profiles_same_username', $sanitizedSameUsernameData );
foreach ( $data['urls'] as $platformKey => $value ) {
$value = sanitize_text_field( $value );
$metaName = 'aioseo_' . aioseo()->helpers->toSnakeCase( $platformKey );
update_user_meta( $userId, $metaName, $value );
}
$additionalUrls = sanitize_text_field( $data['additionalUrls'] );
$sanitizedAdditionalUrls = preg_replace( '/\h/', "\n", (string) $additionalUrls );
update_user_meta( $userId, 'aioseo_profiles_additional_urls', $sanitizedAdditionalUrls );
}
/**
* Returns a list of supported social profiles.
*
* @since 4.2.2
*
* @return array
*/
public function getSocialProfiles() {
return [
'facebookPageUrl' => '',
'twitterUrl' => '',
'instagramUrl' => '',
'tiktokUrl' => '',
'pinterestUrl' => '',
'youtubeUrl' => '',
'linkedinUrl' => '',
'tumblrUrl' => '',
'yelpPageUrl' => '',
'soundCloudUrl' => '',
'wikipediaUrl' => '',
'myspaceUrl' => '',
'wordPressUrl' => '',
'blueskyUrl' => '',
'threadsUrl' => ''
];
}
} Standalone/SeoPreview.php 0000666 00000021725 15113050717 0011450 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
/**
* Handles the SEO Preview feature on the front-end.
*
* @since 4.2.8
*/
class SeoPreview {
/**
* Whether this feature is allowed on the current page or not.
*
* @since 4.2.8
*
* @var bool
*/
private $enable = false;
/**
* The relative JS filename for this standalone.
*
* @since 4.3.1
*
* @var string
*/
private $mainAssetRelativeFilename = 'src/vue/standalone/seo-preview/main.js';
/**
* Class constructor.
*
* @since 4.2.8
*/
public function __construct() {
// Allow users to disable SEO Preview.
if ( apply_filters( 'aioseo_seo_preview_disable', false ) ) {
return;
}
// Hook into `wp` in order to have access to the WP queried object.
add_action( 'wp', [ $this, 'init' ], 20 );
}
/**
* Initialize the feature.
* Hooked into `wp` action hook.
*
* @since 4.2.8
*
* @return void
*/
public function init() {
if (
is_admin() ||
! aioseo()->helpers->isAdminBarEnabled() ||
// If we're seeing the Divi theme Visual Builder.
( function_exists( 'et_core_is_fb_enabled' ) && et_core_is_fb_enabled() ) ||
aioseo()->helpers->isAmpPage()
) {
return;
}
$allow = [
'archive',
'attachment',
'author',
'date',
'dynamic_home',
'page',
'search',
'single',
'taxonomy',
];
if ( ! in_array( aioseo()->helpers->getTemplateType(), $allow, true ) ) {
return;
}
$this->enable = true;
// Prevent Autoptimize from optimizing the translations for the SEO Preview. If we don't do this, Autoptimize can break the frontend for certain languages - #5235.
if ( is_user_logged_in() && 'en_US' !== get_user_locale() ) {
add_filter( 'autoptimize_filter_noptimize', '__return_true' );
}
// As WordPress uses priority 10 to print footer scripts we use 9 to make sure our script still gets output.
add_action( 'wp_print_footer_scripts', [ $this, 'enqueueScript' ], 9 );
}
/**
* Hooked into `wp_print_footer_scripts` action hook.
* Enqueue the standalone JS the latest possible and prevent 3rd-party performance plugins from merging it.
*
* @since 4.3.1
*
* @return void
*/
public function enqueueScript() {
aioseo()->core->assets->load( $this->mainAssetRelativeFilename, [], $this->getVueData(), 'aioseoSeoPreview' );
aioseo()->main->enqueueTranslations();
}
/**
* Returns the data for Vue.
*
* @since 4.2.8
*
* @return array The data.
*/
private function getVueData() {
$data = [
'editGoogleSnippetUrl' => '',
'editFacebookSnippetUrl' => '',
'editTwitterSnippetUrl' => '',
'editObjectBtnText' => '',
'editObjectUrl' => '',
'keyphrases' => '',
'page_analysis' => '',
'urls' => [
'home' => home_url(),
'domain' => aioseo()->helpers->getSiteDomain(),
'mainSiteUrl' => aioseo()->helpers->getSiteUrl(),
],
'mainAssetCssQueue' => aioseo()->core->assets->getJsAssetCssQueue( $this->mainAssetRelativeFilename ),
'data' => [
'isDev' => aioseo()->helpers->isDev(),
'siteName' => aioseo()->helpers->getWebsiteName(),
'usingPermalinks' => aioseo()->helpers->usingPermalinks()
]
];
if ( BuddyPressIntegration::isComponentPage() ) {
return array_merge( $data, aioseo()->standalone->buddyPress->getVueDataSeoPreview() );
}
$queriedObject = get_queried_object(); // Don't use the getTerm helper here.
$templateType = aioseo()->helpers->getTemplateType();
if (
'taxonomy' === $templateType ||
'single' === $templateType ||
'page' === $templateType ||
'attachment' === $templateType
) {
$labels = null;
if ( is_a( $queriedObject, 'WP_Term' ) ) {
$wpObject = $queriedObject;
$labels = get_taxonomy_labels( get_taxonomy( $queriedObject->taxonomy ) );
$data['editObjectUrl'] = get_edit_term_link( $queriedObject, $queriedObject->taxonomy );
} else {
$wpObject = aioseo()->helpers->getPost();
if ( is_a( $wpObject, 'WP_Post' ) ) {
$labels = get_post_type_labels( get_post_type_object( $wpObject->post_type ) );
$data['editObjectUrl'] = get_edit_post_link( $wpObject, 'url' );
if (
! aioseo()->helpers->isSpecialPage( $wpObject->ID ) &&
'attachment' !== $templateType
) {
$aioseoPost = Models\Post::getPost( $wpObject->ID );
$data['page_analysis'] = Models\Post::getPageAnalysisDefaults( $aioseoPost->page_analysis );
$data['keyphrases'] = Models\Post::getKeyphrasesDefaults( $aioseoPost->keyphrases );
}
}
}
// At this point if `$wpObject` is not an instance of WP_Term nor WP_Post, then we can't have the URLs.
if (
is_object( $wpObject ) &&
is_object( $labels )
) {
$data['editObjectBtnText'] = sprintf(
// Translators: 1 - A noun for something that's being edited ("Post", "Page", "Article", "Product", etc.).
esc_html__( 'Edit %1$s', 'all-in-one-seo-pack' ),
$labels->singular_name
);
$data['editGoogleSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'google', $wpObject );
$data['editFacebookSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'facebook', $wpObject );
$data['editTwitterSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'twitter', $wpObject );
}
}
if (
'archive' === $templateType ||
'author' === $templateType ||
'date' === $templateType ||
'search' === $templateType
) {
if ( is_a( $queriedObject, 'WP_User' ) ) {
$data['editObjectUrl'] = get_edit_user_link( $queriedObject->ID );
$data['editObjectBtnText'] = esc_html__( 'Edit User', 'all-in-one-seo-pack' );
}
$data['editGoogleSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'google' );
}
if ( 'dynamic_home' === $templateType ) {
$data['editGoogleSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'google' );
$data['editFacebookSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'facebook' );
$data['editTwitterSnippetUrl'] = $this->getEditSnippetUrl( $templateType, 'twitter' );
}
return $data;
}
/**
* Get the URL to the place where the snippet details can be edited.
*
* @since 4.2.8
*
* @param string $templateType The WP template type {@see WpContext::getTemplateType}.
* @param string $snippet 'google', 'facebook' or 'twitter'.
* @param \WP_Post|\WP_Term|null $object Post or term object.
* @return string The URL. Returns an empty string if nothing matches.
*/
private function getEditSnippetUrl( $templateType, $snippet, $object = null ) {
$url = '';
// Bail if `$snippet` doesn't fit requirements.
if ( ! in_array( $snippet, [ 'google', 'facebook', 'twitter' ], true ) ) {
return $url;
}
// If we're in a post/page/term (not an attachment) we'll have a URL directly to the meta box.
if ( in_array( $templateType, [ 'single', 'page', 'attachment', 'taxonomy' ], true ) ) {
$url = 'taxonomy' === $templateType
? get_edit_term_link( $object, $object->taxonomy ) . '#aioseo-term-settings-field'
: get_edit_post_link( $object, 'url' ) . '#aioseo-settings';
$queryArgs = [ 'aioseo-tab' => 'general' ];
if ( in_array( $snippet, [ 'facebook', 'twitter' ], true ) ) {
$queryArgs = [
'aioseo-tab' => 'social',
'social-tab' => $snippet
];
}
return add_query_arg( $queryArgs, $url );
}
// If we're in any sort of archive let's point to the global archive editing.
if ( in_array( $templateType, [ 'archive', 'author', 'date', 'search' ], true ) ) {
return admin_url( 'admin.php?page=aioseo-search-appearance' ) . '#/archives';
}
// If homepage is set to show the latest posts let's point to the global home page editing.
if ( 'dynamic_home' === $templateType ) {
// Default `$url` for 'google' snippet.
$url = add_query_arg(
[ 'aioseo-scroll' => 'home-page-settings' ],
admin_url( 'admin.php?page=aioseo-search-appearance' ) . '#/global-settings'
);
if ( in_array( $snippet, [ 'facebook', 'twitter' ], true ) ) {
$url = admin_url( 'admin.php?page=aioseo-social-networks' ) . '#/' . $snippet;
}
return $url;
}
return $url;
}
/**
* Returns the "SEO Preview" submenu item data ("node" as WP calls it).
*
* @since 4.2.8
*
* @return array The admin bar menu item data or an empty array if this feature is disabled.
*/
public function getAdminBarMenuItemNode() {
if ( ! $this->enable ) {
return [];
}
$title = esc_html__( 'SEO Preview', 'all-in-one-seo-pack' );
// @TODO Remove 'NEW' after a couple months.
$title .= '<span class="aioseo-menu-new-indicator">';
$title .= esc_html__( 'NEW', 'all-in-one-seo-pack' ) . '!';
$title .= '</span>';
return [
'id' => 'aioseo-seo-preview',
'parent' => 'aioseo-main',
'title' => $title,
'href' => '#',
];
}
} Standalone/BuddyPress/BuddyPress.php 0000666 00000023313 15113050717 0013523 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\BuddyPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
/**
* Handles the BuddyPress integration with AIOSEO.
*
* @since 4.7.6
*/
class BuddyPress {
/**
* Instance of the Tags class.
*
* @since 4.7.6
*
* @var Tags
*/
public $tags;
/**
* Instance of the Component class.
*
* @since 4.7.6
*
* @var Component
*/
public $component;
/**
* Instance of the Sitemap class.
*
* @since 4.7.6
*
* @var Sitemap
*/
public $sitemap = null;
/**
* Class constructor.
*
* @since 4.7.6
*/
public function __construct() {
if (
aioseo()->helpers->isAjaxCronRestRequest() ||
! aioseo()->helpers->isPluginActive( 'buddypress' )
) {
return;
}
// Hook into `plugins_loaded` to ensure BuddyPress has loaded some necessary functions.
add_action( 'plugins_loaded', [ $this, 'maybeLoad' ], 20 );
}
/**
* Hooked into `plugins_loaded` action hook.
*
* @since 4.7.6
*
* @return void
*/
public function maybeLoad() {
// If the BuddyPress version is below 12 we bail.
if ( ! function_exists( 'bp_get_version' ) || version_compare( bp_get_version(), '12', '<' ) ) {
return;
}
// If none of the necessary BuddyPress components are active we bail.
if (
! BuddyPressIntegration::isComponentActive( 'activity' ) &&
! BuddyPressIntegration::isComponentActive( 'group' ) &&
! BuddyPressIntegration::isComponentActive( 'member' )
) {
return;
}
$this->sitemap = new Sitemap();
add_action( 'init', [ $this, 'setTags' ], 20 );
add_action( 'bp_parse_query', [ $this, 'setComponent' ], 20 );
}
/**
* Hooked into `init` action hook.
*
* @since 4.7.6
*
* @return void
*/
public function setTags() {
$this->tags = new Tags();
}
/**
* Hooked into `bp_parse_query` action hook.
*
* @since 4.7.6
*
* @return void
*/
public function setComponent() {
$this->component = new Component();
}
/**
* Adds the BuddyPress fake post types to the list of post types, so they appear under e.g. Search Appearance.
*
* @since 4.7.6
*
* @param array $postTypes Public post types from {@see \AIOSEO\Plugin\Common\Traits\Helpers\Wp::getPublicPostTypes}.
* @param bool $namesOnly Whether only the names should be included.
* @param bool $hasArchivesOnly Whether to only include post types which have archives.
* @param array $args Additional arguments.
* @return void
*/
public function maybeAddPostTypes( &$postTypes, $namesOnly, $hasArchivesOnly, $args ) {
// If one of these CPTs is already registered we bail, so we don't overwrite them and possibly break something.
if (
post_type_exists( 'bp-activity' ) ||
post_type_exists( 'bp-group' ) ||
post_type_exists( 'bp-member' )
) {
return;
}
/**
* The BP components are registered with the `buddypress` CPT which is not viewable, so we add it here to include our metadata inside <head>.
* {@see \AIOSEO\Plugin\Common\Main\Head::wpHead}.
*/
if (
$namesOnly &&
doing_action( 'wp_head' )
) {
$postTypes = array_merge( $postTypes, [ 'buddypress' ] );
return;
}
$fakePostTypes = $this->getFakePostTypes();
if ( ! BuddyPressIntegration::isComponentActive( 'activity' ) ) {
unset( $fakePostTypes['bp-activity'] );
}
if ( ! BuddyPressIntegration::isComponentActive( 'group' ) ) {
unset( $fakePostTypes['bp-group'] );
}
if ( ! BuddyPressIntegration::isComponentActive( 'member' ) ) {
unset( $fakePostTypes['bp-member'] );
}
if ( $hasArchivesOnly ) {
$fakePostTypes = array_filter( $fakePostTypes, function ( $postType ) {
return $postType['hasArchive'];
} );
}
if ( $namesOnly ) {
$fakePostTypes = array_keys( $fakePostTypes );
}
// 0. Below we'll add/merge the BuddyPress post types only under certain conditions.
$fakePostTypes = array_values( $fakePostTypes );
$currentScreen = aioseo()->helpers->getCurrentScreen();
if (
// 1. If the `buddypress` CPT is set in the list of post types to be included.
( ! empty( $args['include'] ) && in_array( 'buddypress', $args['include'], true ) ) ||
// 2. If the current request is for the sitemap.
( ! empty( aioseo()->sitemap->filename ) && 'general' === ( aioseo()->sitemap->type ?? '' ) ) ||
// 3. If we're on the Search Appearance screen.
( $currentScreen && strpos( $currentScreen->id, 'aioseo-search-appearance' ) !== false ) ||
// 4. If we're on the BuddyPress component front-end screen.
BuddyPressIntegration::isComponentPage()
) {
$postTypes = array_merge( $postTypes, $fakePostTypes );
}
}
/**
* Get edit links for the SEO Preview data.
*
* @since 4.7.6
*
* @return array
*/
public function getVueDataSeoPreview() {
$data = [
'editGoogleSnippetUrl' => '',
'editObjectBtnText' => '',
'editObjectUrl' => '',
];
list( $postType, $suffix ) = explode( '_', aioseo()->standalone->buddyPress->component->templateType );
$bpFakePostTypes = $this->getFakePostTypes();
$fakePostTypeData = array_values( wp_list_filter( $bpFakePostTypes, [ 'name' => $postType ] ) );
$fakePostTypeData = $fakePostTypeData[0] ?? [];
if ( ! $fakePostTypeData ) {
return $data;
}
if ( 'single' === $suffix ) {
switch ( $postType ) {
case 'bp-activity':
$componentId = aioseo()->standalone->buddyPress->component->activity['id'];
break;
case 'bp-group':
$componentId = aioseo()->standalone->buddyPress->component->group['id'];
break;
case 'bp-member':
$componentId = aioseo()->standalone->buddyPress->component->author->ID;
break;
default:
$componentId = 0;
}
}
$scrollToId = 'aioseo-card-' . $postType . ( 'single' === $suffix ? 'SA' : 'ArchiveArchives' );
$data['editGoogleSnippetUrl'] = 'single' === $suffix
? admin_url( 'admin.php?page=aioseo-search-appearance' ) . '#/content-types'
: admin_url( 'admin.php?page=aioseo-search-appearance' ) . '#/archives';
$data['editGoogleSnippetUrl'] = add_query_arg( [
'aioseo-scroll' => $scrollToId,
'aioseo-highlight' => $scrollToId
], $data['editGoogleSnippetUrl'] );
$data['editObjectBtnText'] = sprintf(
// Translators: 1 - A noun for something that's being edited ("Post", "Page", "Article", "Product", etc.).
esc_html__( 'Edit %1$s', 'all-in-one-seo-pack' ),
'single' === $suffix ? $fakePostTypeData['singular'] : $fakePostTypeData['label']
);
list( , $component ) = explode( '-', $postType );
$data['editObjectUrl'] = 'single' === $suffix
? BuddyPressIntegration::getComponentEditUrl( $component, $componentId ?? 0 )
: BuddyPressIntegration::callFunc( 'bp_get_admin_url', add_query_arg( 'page', 'bp-rewrites', 'admin.php' ) );
return $data;
}
/**
* Retrieves the BuddyPress fake post types.
*
* @since 4.7.6
*
* @return array The BuddyPress fake post types.
*/
public function getFakePostTypes() {
return [
'bp-activity' => [
'name' => 'bp-activity',
'label' => sprintf(
// Translators: 1 - The hard coded string 'BuddyPress'.
_x( 'Activities (%1$s)', 'BuddyPress', 'all-in-one-seo-pack' ),
'BuddyPress'
),
'singular' => 'Activity',
'icon' => 'dashicons-buddicons-buddypress-logo',
'hasExcerpt' => false,
'hasArchive' => true,
'hierarchical' => false,
'taxonomies' => [],
'slug' => 'bp-activity',
'buddyPress' => true,
'defaultTags' => [
'postTypes' => [
'title' => [
'bp_activity_action',
'separator_sa',
'site_title',
],
'description' => [
'bp_activity_content',
'separator_sa'
]
]
],
'defaultTitle' => '#bp_activity_action #separator_sa #site_title',
'defaultDescription' => '#bp_activity_content',
],
'bp-group' => [
'name' => 'bp-group',
'label' => sprintf(
// Translators: 1 - The hard coded string 'BuddyPress'.
_x( 'Groups (%1$s)', 'BuddyPress', 'all-in-one-seo-pack' ),
'BuddyPress'
),
'singular' => 'Group',
'icon' => 'dashicons-buddicons-buddypress-logo',
'hasExcerpt' => false,
'hasArchive' => true,
'hierarchical' => false,
'taxonomies' => [],
'slug' => 'bp-group',
'buddyPress' => true,
'defaultTags' => [
'postTypes' => [
'title' => [
'bp_group_name',
'separator_sa',
'site_title',
],
'description' => [
'bp_group_description',
'separator_sa'
]
]
],
'defaultTitle' => '#bp_group_name #separator_sa #site_title',
'defaultDescription' => '#bp_group_description',
],
'bp-member' => [
'name' => 'bp-member',
'label' => sprintf(
// Translators: 1 - The hard coded string 'BuddyPress'.
_x( 'Members (%1$s)', 'BuddyPress', 'all-in-one-seo-pack' ),
'BuddyPress'
),
'singular' => 'Member',
'icon' => 'dashicons-buddicons-buddypress-logo',
'hasExcerpt' => false,
'hasArchive' => true,
'hierarchical' => false,
'taxonomies' => [],
'slug' => 'bp-member',
'buddyPress' => true,
'defaultTags' => [
'postTypes' => [
'title' => [
'author_name',
'separator_sa',
'site_title',
],
'description' => [
'author_bio',
'separator_sa'
]
]
],
'defaultTitle' => '#author_name #separator_sa #site_title',
'defaultDescription' => '#author_bio',
],
];
}
} Standalone/BuddyPress/Sitemap.php 0000666 00000015654 15113050717 0013052 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\BuddyPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
/**
* BuddyPress Sitemap class.
*
* @since 4.7.6
*/
class Sitemap {
/**
* Returns the indexes for the sitemap root index.
*
* @since 4.7.6
*
* @return array The indexes.
*/
public function indexes() {
$indexes = [];
$includedPostTypes = array_flip( aioseo()->sitemap->helpers->includedPostTypes() );
$filterPostTypes = array_filter( [
BuddyPressIntegration::isComponentActive( 'activity' ) && isset( $includedPostTypes['bp-activity'] ) ? 'bp-activity' : '',
BuddyPressIntegration::isComponentActive( 'group' ) && isset( $includedPostTypes['bp-group'] ) ? 'bp-group' : '',
BuddyPressIntegration::isComponentActive( 'member' ) && isset( $includedPostTypes['bp-member'] ) ? 'bp-member' : '',
] );
foreach ( $filterPostTypes as $postType ) {
$indexes = array_merge( $indexes, $this->buildIndexesPostType( $postType ) );
}
return $indexes;
}
/**
* Builds BuddyPress related root indexes.
*
* @since 4.7.6
*
* @param string $postType The BuddyPress fake post type.
* @return array The BuddyPress related root indexes.
*/
private function buildIndexesPostType( $postType ) {
switch ( $postType ) {
case 'bp-activity':
return $this->buildIndexesActivity();
case 'bp-group':
return $this->buildIndexesGroup();
case 'bp-member':
return $this->buildIndexesMember();
default:
return [];
}
}
/**
* Builds activity root indexes.
*
* @since 4.7.6
*
* @return array The activity root indexes.
*/
private function buildIndexesActivity() {
$activityTable = aioseo()->core->db->prefix . 'bp_activity';
$linksPerIndex = aioseo()->sitemap->linksPerIndex;
$items = aioseo()->core->db->execute(
aioseo()->core->db->db->prepare(
"SELECT id, date_recorded
FROM (
SELECT @row := @row + 1 AS rownum, id, date_recorded
FROM (
SELECT a.id, a.date_recorded FROM $activityTable as a
WHERE a.is_spam = 0
AND a.hide_sitewide = 0
AND a.type NOT IN ('activity_comment', 'last_activity')
ORDER BY a.date_recorded DESC
) AS x
CROSS JOIN (SELECT @row := 0) AS vars
ORDER BY date_recorded DESC
) AS y
WHERE rownum = 1 OR rownum % %d = 1;",
[
$linksPerIndex
]
),
true
)->result();
$totalItems = aioseo()->core->db->execute(
"SELECT COUNT(*) as count
FROM $activityTable as a
WHERE a.is_spam = 0
AND a.hide_sitewide = 0
AND a.type NOT IN ('activity_comment', 'last_activity')
",
true
)->result();
$indexes = [];
if ( $items ) {
$filename = aioseo()->sitemap->filename;
$count = count( $items );
for ( $i = 0; $i < $count; $i++ ) {
$indexNumber = 0 !== $i && 1 < $count ? $i + 1 : '';
$indexes[] = [
'loc' => aioseo()->helpers->localizedUrl( "/bp-activity-$filename$indexNumber.xml" ),
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $items[ $i ]->date_recorded ),
'count' => $linksPerIndex
];
}
// We need to update the count of the last index since it won't necessarily be the same as the links per index.
$indexes[ count( $indexes ) - 1 ]['count'] = $totalItems[0]->count - ( $linksPerIndex * ( $count - 1 ) );
}
return $indexes;
}
/**
* Builds group root indexes.
*
* @since 4.7.6
*
* @return array The group root indexes.
*/
private function buildIndexesGroup() {
$groupsTable = aioseo()->core->db->prefix . 'bp_groups';
$groupsMetaTable = aioseo()->core->db->prefix . 'bp_groups_groupmeta';
$linksPerIndex = aioseo()->sitemap->linksPerIndex;
$items = aioseo()->core->db->execute(
aioseo()->core->db->db->prepare(
"SELECT id, date_modified
FROM (
SELECT @row := @row + 1 AS rownum, id, date_modified
FROM (
SELECT g.id, gm.group_id, MAX(gm.meta_value) as date_modified FROM $groupsTable as g
INNER JOIN $groupsMetaTable AS gm ON g.id = gm.group_id
WHERE g.status = 'public'
AND gm.meta_key = 'last_activity'
GROUP BY g.id
ORDER BY date_modified DESC
) AS x
CROSS JOIN (SELECT @row := 0) AS vars
ORDER BY date_modified DESC
) AS y
WHERE rownum = 1 OR rownum % %d = 1;",
[
$linksPerIndex
]
),
true
)->result();
$totalItems = aioseo()->core->db->execute(
"SELECT COUNT(*) as count
FROM $groupsTable as g
WHERE g.status = 'public'
",
true
)->result();
$indexes = [];
if ( $items ) {
$filename = aioseo()->sitemap->filename;
$count = count( $items );
for ( $i = 0; $i < $count; $i++ ) {
$indexNumber = 0 !== $i && 1 < $count ? $i + 1 : '';
$indexes[] = [
'loc' => aioseo()->helpers->localizedUrl( "/bp-group-$filename$indexNumber.xml" ),
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $items[ $i ]->date_modified ),
'count' => $linksPerIndex
];
}
// We need to update the count of the last index since it won't necessarily be the same as the links per index.
$indexes[ count( $indexes ) - 1 ]['count'] = $totalItems[0]->count - ( $linksPerIndex * ( $count - 1 ) );
}
return $indexes;
}
/**
* Builds member root indexes.
*
* @since 4.7.6
*
* @return array The member root indexes.
*/
private function buildIndexesMember() {
$activityTable = aioseo()->core->db->prefix . 'bp_activity';
$linksPerIndex = aioseo()->sitemap->linksPerIndex;
$items = aioseo()->core->db->execute(
aioseo()->core->db->db->prepare(
"SELECT user_id, date_recorded
FROM (
SELECT @row := @row + 1 AS rownum, user_id, date_recorded
FROM (
SELECT a.user_id, a.date_recorded FROM $activityTable as a
WHERE a.component = 'members'
AND a.type = 'last_activity'
ORDER BY a.date_recorded DESC
) AS x
CROSS JOIN (SELECT @row := 0) AS vars
ORDER BY date_recorded DESC
) AS y
WHERE rownum = 1 OR rownum % %d = 1;",
[
$linksPerIndex
]
),
true
)->result();
$totalItems = aioseo()->core->db->execute(
"SELECT COUNT(*) as count
FROM $activityTable as a
WHERE a.component = 'members'
AND a.type = 'last_activity'
",
true
)->result();
$indexes = [];
if ( $items ) {
$filename = aioseo()->sitemap->filename;
$count = count( $items );
for ( $i = 0; $i < $count; $i++ ) {
$indexNumber = 0 !== $i && 1 < $count ? $i + 1 : '';
$indexes[] = [
'loc' => aioseo()->helpers->localizedUrl( "/bp-member-$filename$indexNumber.xml" ),
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $items[ $i ]->date_recorded ),
'count' => $linksPerIndex
];
}
// We need to update the count of the last index since it won't necessarily be the same as the links per index.
$indexes[ count( $indexes ) - 1 ]['count'] = $totalItems[0]->count - ( $linksPerIndex * ( $count - 1 ) );
}
return $indexes;
}
} Standalone/BuddyPress/Component.php 0000666 00000030273 15113050717 0013404 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\BuddyPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
use AIOSEO\Plugin\Common\Schema\Graphs as CommonGraphs;
/**
* BuddyPress Component class.
*
* @since 4.7.6
*/
class Component {
/**
* The current component template type.
*
* @since 4.7.6
*
* @var string|null
*/
public $templateType = null;
/**
* The component ID.
*
* @since 4.7.6
*
* @var int
*/
public $id = 0;
/**
* The component author.
*
* @since 4.7.6
*
* @var \WP_User|false
*/
public $author = false;
/**
* The component date.
*
* @since 4.7.6
*
* @var int|false
*/
public $date = false;
/**
* The activity single page data.
*
* @since 4.7.6
*
* @var array
*/
public $activity = [];
/**
* The group single page data.
*
* @since 4.7.6
*
* @var array
*/
public $group = [];
/**
* The type of the group archive page.
*
* @since 4.7.6
*
* @var array
*/
public $groupType = [];
/**
* Class constructor.
*
* @since 4.7.6
*/
public function __construct() {
if ( is_admin() ) {
return;
}
$this->setTemplateType();
$this->setId();
$this->setAuthor();
$this->setDate();
$this->setActivity();
$this->setGroup();
$this->setGroupType();
}
/**
* Sets the template type.
*
* @since 4.7.6
*
* @return void
*/
private function setTemplateType() {
if ( BuddyPressIntegration::callFunc( 'bp_is_single_activity' ) ) {
$this->templateType = 'bp-activity_single';
} elseif ( BuddyPressIntegration::callFunc( 'bp_is_group' ) ) {
$this->templateType = 'bp-group_single';
} elseif (
BuddyPressIntegration::callFunc( 'bp_is_user' ) &&
false === BuddyPressIntegration::callFunc( 'bp_is_single_activity' )
) {
$this->templateType = 'bp-member_single';
} elseif ( BuddyPressIntegration::callFunc( 'bp_is_activity_directory' ) ) {
$this->templateType = 'bp-activity_archive';
} elseif ( BuddyPressIntegration::callFunc( 'bp_is_members_directory' ) ) {
$this->templateType = 'bp-member_archive';
} elseif ( BuddyPressIntegration::callFunc( 'bp_is_groups_directory' ) ) {
$this->templateType = 'bp-group_archive';
} elseif (
BuddyPressIntegration::callFunc( 'bp_is_current_action', 'feed' ) &&
BuddyPressIntegration::callFunc( 'bp_is_activity_component' )
) {
$this->templateType = 'bp-activity_feed';
}
}
/**
* Sets the component ID.
*
* @since 4.7.6
*
* @return void
*/
private function setId() {
switch ( $this->templateType ) {
case 'bp-activity_single':
$id = get_query_var( 'bp_member_action' );
break;
case 'bp-group_single':
$id = get_query_var( 'bp_group' );
break;
case 'bp-member_single':
$id = get_query_var( 'bp_member' );
break;
default:
$id = $this->id;
}
$this->id = $id;
}
/**
* Sets the component author.
*
* @since 4.7.6
*
* @return void
*/
private function setAuthor() {
switch ( $this->templateType ) {
case 'bp-activity_single':
if ( ! $this->activity ) {
$this->setActivity();
}
if ( $this->activity ) {
$this->author = get_user_by( 'id', $this->activity['user_id'] );
return;
}
break;
case 'bp-group_single':
if ( ! $this->group ) {
$this->setGroup();
}
if ( $this->group ) {
$this->author = get_user_by( 'id', $this->group['creator_id'] );
return;
}
break;
case 'bp-member_single':
$this->author = get_user_by( 'slug', $this->id );
return;
}
}
/**
* Sets the component date.
*
* @since 4.7.6
*
* @return void
*/
private function setDate() {
switch ( $this->templateType ) {
case 'bp-activity_single':
if ( ! $this->activity ) {
$this->setActivity();
}
$date = strtotime( $this->activity['date_recorded'] );
break;
case 'bp-group_single':
if ( ! $this->group ) {
$this->setGroup();
}
$date = strtotime( $this->group['date_created'] );
break;
default:
$date = $this->date;
}
$this->date = $date;
}
/**
* Sets the activity data.
*
* @since 4.7.6
*
* @return void
*/
private function setActivity() {
if ( 'bp-activity_single' !== $this->templateType ) {
return;
}
$activities = BuddyPressIntegration::callFunc( 'bp_activity_get_specific', [
'activity_ids' => [ $this->id ],
'display_comments' => true
] );
if ( ! empty( $activities['activities'] ) ) {
list( $activity ) = current( $activities );
$this->activity = (array) $activity;
// The `content_rendered` is AIOSEO specific.
$this->activity['content_rendered'] = $this->activity['content'] ?? '';
if ( ! empty( $this->activity['content'] ) ) {
$this->activity['content_rendered'] = apply_filters( 'bp_get_activity_content', $this->activity['content'] );
}
return;
}
$this->resetComponent();
}
/**
* Sets the group data.
*
* @since 4.7.6
*
* @return void
*/
private function setGroup() {
if ( 'bp-group_single' !== $this->templateType ) {
return;
}
$group = BuddyPressIntegration::callFunc( 'bp_get_group_by', 'slug', $this->id );
if ( ! empty( $group ) ) {
$this->group = (array) $group;
return;
}
$this->resetComponent();
}
/**
* Sets the group type.
*
* @since 4.7.6
*
* @return void
*/
private function setGroupType() {
if ( 'bp-group_archive' !== $this->templateType ) {
return;
}
$type = BuddyPressIntegration::callFunc( 'bp_get_current_group_directory_type' );
if ( ! $type ) {
return;
}
$term = get_term_by( 'slug', $type, 'bp_group_type' );
if ( ! $term ) {
return;
}
$meta = get_metadata( 'term', $term->term_id );
if ( ! $meta ) {
return;
}
$this->groupType = [
'singular' => $meta['bp_type_singular_name'][0] ?? '',
'plural' => $meta['bp_type_name'][0] ?? '',
];
}
/**
* Resets some of the component properties.
*
* @since 4.7.6
*
* @return void
*/
private function resetComponent() {
$this->templateType = null;
$this->id = 0;
}
/**
* Retrieves the SEO metadata value.
*
* @since 4.7.6
*
* @param string $which The SEO metadata to get.
* @return string The SEO metadata value.
*/
public function getMeta( $which ) {
list( $postType, $suffix ) = explode( '_', $this->templateType );
switch ( $which ) {
case 'title':
$meta = 'single' === $suffix
? aioseo()->meta->title->getPostTypeTitle( $postType )
: aioseo()->meta->title->getArchiveTitle( $postType );
$meta = aioseo()->meta->description->helpers->bpSanitize( $meta, $this->id );
break;
case 'description':
$meta = 'single' === $suffix
? aioseo()->meta->description->getPostTypeDescription( $postType )
: aioseo()->meta->description->getArchiveDescription( $postType );
$meta = aioseo()->meta->description->helpers->bpSanitize( $meta, $this->id );
break;
case 'keywords':
$meta = 'single' === $suffix
? ''
: aioseo()->meta->keywords->getArchiveKeywords( $postType );
$meta = aioseo()->meta->keywords->prepareKeywords( $meta );
break;
case 'robots':
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
if ( 'single' === $suffix && $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
aioseo()->meta->robots->globalValues( [ 'postTypes', $postType ], true );
} elseif ( $dynamicOptions->searchAppearance->archives->has( $postType ) ) {
aioseo()->meta->robots->globalValues( [ 'archives', $postType ], true );
}
$meta = aioseo()->meta->robots->metaHelper();
break;
case 'canonical':
$meta = '';
if ( 'single' === $suffix ) {
if ( 'bp-member' === $postType ) {
$meta = BuddyPressIntegration::getComponentSingleUrl( 'member', $this->author->ID );
} elseif ( 'bp-group' === $postType ) {
$meta = BuddyPressIntegration::getComponentSingleUrl( 'group', $this->group['id'] );
}
}
break;
default:
$meta = '';
}
return $meta;
}
/**
* Determines the schema type for the current component.
*
* @since 4.7.6
*
* @param \AIOSEO\Plugin\Common\Schema\Context $contextInstance The Context class instance.
* @return void
*/
public function determineSchemaGraphsAndContext( $contextInstance ) {
list( $postType ) = explode( '_', $this->templateType );
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
if ( $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
$defaultType = $dynamicOptions->searchAppearance->postTypes->{$postType}->schemaType;
switch ( $defaultType ) {
case 'Article':
aioseo()->schema->graphs[] = $dynamicOptions->searchAppearance->postTypes->{$postType}->articleType;
break;
case 'WebPage':
aioseo()->schema->graphs[] = $dynamicOptions->searchAppearance->postTypes->{$postType}->webPageType;
break;
default:
aioseo()->schema->graphs[] = $defaultType;
}
}
switch ( $this->templateType ) {
case 'bp-activity_single':
$datePublished = $this->activity['date_recorded'];
$contextUrl = BuddyPressIntegration::getComponentSingleUrl( 'activity', $this->activity['id'] );
break;
case 'bp-group_single':
$datePublished = $this->group['date_created'];
$contextUrl = BuddyPressIntegration::getComponentSingleUrl( 'group', $this->group['id'] );
break;
case 'bp-member_single':
aioseo()->schema->graphs[] = 'ProfilePage';
$contextUrl = BuddyPressIntegration::getComponentSingleUrl( 'member', $this->author->ID );
break;
case 'bp-activity_archive':
case 'bp-group_archive':
case 'bp-member_archive':
list( , $component ) = explode( '-', $postType );
$contextUrl = BuddyPressIntegration::getComponentArchiveUrl( $component );
$breadcrumbType = 'CollectionPage';
break;
default:
break;
}
if ( ! empty( $datePublished ) ) {
CommonGraphs\Article\NewsArticle::setOverwriteGraphData( [
'properties' => compact( 'datePublished' )
] );
}
if ( ! empty( $contextUrl ) ) {
$name = aioseo()->meta->title->getTitle();
$description = aioseo()->meta->description->getDescription();
$breadcrumbPositions = [
'name' => $name,
'description' => $description,
'url' => $contextUrl,
];
if ( ! empty( $breadcrumbType ) ) {
$breadcrumbPositions['type'] = $breadcrumbType;
}
aioseo()->schema->context = [
'name' => $name,
'description' => $description,
'url' => $contextUrl,
'breadcrumb' => $contextInstance->breadcrumb->setPositions( $breadcrumbPositions ),
];
}
}
/**
* Gets the breadcrumbs for the current component.
*
* @since 4.7.6
*
* @return array
*/
public function getCrumbs() {
$crumbs = [];
switch ( $this->templateType ) {
case 'bp-activity_single':
$crumbs[] = aioseo()->breadcrumbs->makeCrumb(
BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'activity' ),
BuddyPressIntegration::getComponentArchiveUrl( 'activity' )
);
$crumbs[] = aioseo()->breadcrumbs->makeCrumb( sanitize_text_field( $this->activity['action'] ) );
break;
case 'bp-group_single':
$crumbs[] = aioseo()->breadcrumbs->makeCrumb(
BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'groups' ),
BuddyPressIntegration::getComponentArchiveUrl( 'group' )
);
$crumbs[] = aioseo()->breadcrumbs->makeCrumb( $this->group['name'] );
break;
case 'bp-member_single':
$crumbs[] = aioseo()->breadcrumbs->makeCrumb(
BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'members' ),
BuddyPressIntegration::getComponentArchiveUrl( 'member' )
);
$crumbs[] = aioseo()->breadcrumbs->makeCrumb( $this->author->display_name );
break;
case 'bp-activity_archive':
$crumbs[] = aioseo()->breadcrumbs->makeCrumb( BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'activity' ) );
break;
case 'bp-group_archive':
$crumbs[] = aioseo()->breadcrumbs->makeCrumb( BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'groups' ) );
break;
case 'bp-member_archive':
$crumbs[] = aioseo()->breadcrumbs->makeCrumb( BuddyPressIntegration::callFunc( 'bp_get_directory_title', 'members' ) );
break;
default:
break;
}
return $crumbs;
}
} Standalone/BuddyPress/Tags.php 0000666 00000023531 15113050717 0012337 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\BuddyPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* BuddyPress Tags class.
*
* @since 4.7.6
*/
class Tags {
/**
* Class constructor.
*
* @since 4.7.6
*/
public function __construct() {
aioseo()->tags->addContext( $this->getContexts() );
aioseo()->tags->addTags( $this->getTags() );
}
/**
* Retrieves the contexts for BuddyPress.
*
* @since 4.7.6
*
* @return array An array of contextual data.
*/
public function getContexts() {
return [
'bp-activityTitle' => [
'author_first_name',
'author_last_name',
'author_name',
'current_date',
'current_day',
'current_month',
'current_year',
'post_date',
'post_day',
'post_month',
'post_year',
'separator_sa',
'site_title',
'tagline',
'bp_activity_action',
'bp_activity_content',
],
'bp-activityArchiveTitle' => [
'current_date',
'current_day',
'current_month',
'current_year',
'separator_sa',
'site_title',
'tagline',
'archive_title',
],
'bp-activityDescription' => [
'author_first_name',
'author_last_name',
'author_name',
'current_date',
'current_day',
'current_month',
'current_year',
'post_date',
'post_day',
'post_month',
'post_year',
'separator_sa',
'site_title',
'tagline',
'bp_activity_action',
'bp_activity_content',
],
'bp-activityArchiveDescription' => [
'current_date',
'current_day',
'current_month',
'current_year',
'separator_sa',
'site_title',
'tagline',
'archive_title',
],
'bp-groupTitle' => [
'author_first_name',
'author_last_name',
'author_name',
'current_date',
'current_day',
'current_month',
'current_year',
'post_date',
'post_day',
'post_month',
'post_year',
'separator_sa',
'site_title',
'tagline',
'bp_group_name',
'bp_group_description',
],
'bp-groupArchiveTitle' => [
'current_date',
'current_day',
'current_month',
'current_year',
'separator_sa',
'site_title',
'tagline',
'archive_title',
'bp_group_type_singular_name',
'bp_group_type_plural_name',
],
'bp-groupDescription' => [
'author_first_name',
'author_last_name',
'author_name',
'current_date',
'current_day',
'current_month',
'current_year',
'post_date',
'post_day',
'post_month',
'post_year',
'separator_sa',
'site_title',
'tagline',
'bp_group_name',
'bp_group_description',
],
'bp-groupArchiveDescription' => [
'current_date',
'current_day',
'current_month',
'current_year',
'separator_sa',
'site_title',
'tagline',
'archive_title',
'bp_group_type_singular_name',
'bp_group_type_plural_name',
],
'bp-memberTitle' => [
'author_first_name',
'author_last_name',
'author_name',
'current_date',
'current_day',
'current_month',
'current_year',
'separator_sa',
'site_title',
'tagline',
],
'bp-memberArchiveTitle' => [
'current_date',
'current_day',
'current_month',
'current_year',
'separator_sa',
'site_title',
'tagline',
'archive_title',
],
'bp-memberDescription' => [
'author_first_name',
'author_last_name',
'author_name',
'author_bio',
'current_date',
'current_day',
'current_month',
'current_year',
'separator_sa',
'site_title',
'tagline',
],
'bp-memberArchiveDescription' => [
'current_date',
'current_day',
'current_month',
'current_year',
'separator_sa',
'site_title',
'tagline',
'archive_title',
],
];
}
/**
* Retrieves the custom tags for BuddyPress.
*
* @since 4.7.6
*
* @return array An array of tags.
*/
public function getTags() {
return [
[
'id' => 'bp_activity_action',
'name' => _x( 'Activity Action', 'BuddyPress', 'all-in-one-seo-pack' ),
'description' => _x( 'The activity action.', 'BuddyPress', 'all-in-one-seo-pack' ),
'instance' => $this,
],
[
'id' => 'bp_activity_content',
'name' => _x( 'Activity Content', 'BuddyPress', 'all-in-one-seo-pack' ),
'description' => _x( 'The activity content.', 'BuddyPress', 'all-in-one-seo-pack' ),
'instance' => $this,
],
[
'id' => 'bp_group_name',
'name' => _x( 'Group Name', 'BuddyPress', 'all-in-one-seo-pack' ),
'description' => _x( 'The group name.', 'BuddyPress', 'all-in-one-seo-pack' ),
'instance' => $this,
],
[
'id' => 'bp_group_description',
'name' => _x( 'Group Description', 'BuddyPress', 'all-in-one-seo-pack' ),
'description' => _x( 'The group description.', 'BuddyPress', 'all-in-one-seo-pack' ),
'instance' => $this,
],
[
'id' => 'bp_group_type_singular_name',
'name' => _x( 'Group Type Singular Name', 'BuddyPress', 'all-in-one-seo-pack' ),
'description' => _x( 'The group type singular name.', 'BuddyPress', 'all-in-one-seo-pack' ),
'instance' => $this,
],
[
'id' => 'bp_group_type_plural_name',
'name' => _x( 'Group Type Plural Name', 'BuddyPress', 'all-in-one-seo-pack' ),
'description' => _x( 'The group type plural name.', 'BuddyPress', 'all-in-one-seo-pack' ),
'instance' => $this,
],
];
}
/**
* Replace the tags in the string provided.
*
* @since 4.7.6
*
* @param string $string The string to look for tags in.
* @param int $id The object ID.
* @return string The string with tags replaced.
*/
public function replaceTags( $string, $id ) {
if ( ! $string || ! preg_match( '/' . aioseo()->tags->denotationChar . '/', $string ) ) {
return $string;
}
foreach ( array_unique( aioseo()->helpers->flatten( $this->getContexts() ) ) as $tag ) {
$tagId = aioseo()->tags->denotationChar . $tag;
$pattern = "/$tagId(?![a-zA-Z0-9_])/im";
if ( preg_match( $pattern, $string ) ) {
$tagValue = $this->getTagValue( [ 'id' => $tag ], $id );
$string = preg_replace( $pattern, '%|%' . aioseo()->helpers->escapeRegexReplacement( $tagValue ), $string );
}
}
return str_replace( '%|%', '', $string );
}
/**
* Get the value of the tag to replace.
*
* @since 4.7.6
*
* @param array $tag The tag to look for.
* @param int|null $id The object ID.
* @param bool $sampleData Whether to fill empty values with sample data.
* @return string The value of the tag.
*/
public function getTagValue( $tag, $id = null, $sampleData = false ) {
$sampleData = $sampleData || empty( aioseo()->standalone->buddyPress->component->templateType );
switch ( $tag['id'] ) {
case 'author_bio':
$out = $sampleData
? __( 'Sample author biography', 'all-in-one-seo-pack' )
: aioseo()->standalone->buddyPress->component->author->description;
break;
case 'author_first_name':
$out = $sampleData
? wp_get_current_user()->first_name
: aioseo()->standalone->buddyPress->component->author->first_name;
break;
case 'author_last_name':
$out = $sampleData
? wp_get_current_user()->last_name
: aioseo()->standalone->buddyPress->component->author->last_name;
break;
case 'author_name':
$out = $sampleData
? wp_get_current_user()->display_name
: aioseo()->standalone->buddyPress->component->author->display_name;
break;
case 'post_date':
$out = $sampleData
? aioseo()->tags->formatDateAsI18n( date_i18n( 'U' ) )
: aioseo()->tags->formatDateAsI18n( aioseo()->standalone->buddyPress->component->date );
break;
case 'post_day':
$out = $sampleData
? date_i18n( 'd' )
: date( 'd', aioseo()->standalone->buddyPress->component->date );
break;
case 'post_month':
$out = $sampleData
? date_i18n( 'F' )
: date( 'F', aioseo()->standalone->buddyPress->component->date );
break;
case 'post_year':
$out = $sampleData
? date_i18n( 'Y' )
: date( 'Y', aioseo()->standalone->buddyPress->component->date );
break;
case 'archive_title':
$out = $sampleData
? __( 'Sample Archive Title', 'all-in-one-seo-pack' )
: esc_html( get_the_title() );
break;
case 'bp_activity_action':
$out = $sampleData
? _x( 'Sample Activity Action', 'BuddyPress', 'all-in-one-seo-pack' )
: aioseo()->standalone->buddyPress->component->activity['action'];
break;
case 'bp_activity_content':
$out = $sampleData
? _x( 'Sample activity content', 'BuddyPress', 'all-in-one-seo-pack' )
: aioseo()->standalone->buddyPress->component->activity['content_rendered'];
break;
case 'bp_group_name':
$out = $sampleData
? _x( 'Sample Group Name', 'BuddyPress', 'all-in-one-seo-pack' )
: aioseo()->standalone->buddyPress->component->group['name'];
break;
case 'bp_group_description':
$out = $sampleData
? _x( 'Sample group description', 'BuddyPress', 'all-in-one-seo-pack' )
: aioseo()->standalone->buddyPress->component->group['description'];
break;
case 'bp_group_type_singular_name':
$out = $sampleData ? _x( 'Sample Type Singular', 'BuddyPress', 'all-in-one-seo-pack' ) : '';
if ( ! empty( aioseo()->standalone->buddyPress->component->groupType ) ) {
$out = aioseo()->standalone->buddyPress->component->groupType['singular'];
}
break;
case 'bp_group_type_plural_name':
$out = $sampleData ? _x( 'Sample Type Plural', 'BuddyPress', 'all-in-one-seo-pack' ) : '';
if ( ! empty( aioseo()->standalone->buddyPress->component->groupType ) ) {
$out = aioseo()->standalone->buddyPress->component->groupType['plural'];
}
break;
default:
$out = aioseo()->tags->getTagValue( $tag, $id );
}
return $out ?? '';
}
} Standalone/WpCode.php 0000666 00000001354 15113050717 0010535 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles registering the AIOSEO username in the WPCode snippets library.
*
* @since 4.3.8
*/
class WpCode {
/**
* Class constructor.
*
* @since 4.3.8
*/
public function __construct() {
if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
return;
}
add_action( 'wpcode_loaded', [ $this, 'registerUsername' ] );
}
/**
* Enqueues the script.
*
* @since 4.3.8
*
* @return void
*/
public function registerUsername() {
if ( ! function_exists( 'wpcode_register_library_username' ) ) {
return;
}
wpcode_register_library_username( 'aioseo', AIOSEO_PLUGIN_SHORT_NAME );
}
} Standalone/SetupWizard.php 0000666 00000015073 15113050717 0011640 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class that holds our setup wizard.
*
* @since 4.0.0
*/
class SetupWizard {
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
if ( ! is_admin() || wp_doing_cron() || wp_doing_ajax() ) {
return;
}
add_action( 'admin_menu', [ $this, 'addDashboardPage' ] );
add_action( 'admin_head', [ $this, 'hideDashboardPageFromMenu' ] );
add_action( 'admin_init', [ $this, 'maybeLoadOnboardingWizard' ] );
add_action( 'admin_init', [ $this, 'redirect' ], 9999 );
}
/**
* Onboarding Wizard redirect.
*
* This function checks if a new install or update has just occurred. If so,
* then we redirect the user to the appropriate page.
*
* @since 4.0.0
*
* @return void
*/
public function redirect() {
// Check if we should consider redirection.
if ( ! aioseo()->core->cache->get( 'activation_redirect' ) ) {
return;
}
// If we are redirecting, clear the transient so it only happens once.
aioseo()->core->cache->delete( 'activation_redirect' );
// Check option to disable welcome redirect.
if ( get_option( 'aioseo_activation_redirect', false ) ) {
return;
}
// Only do this for single site installs.
if ( isset( $_GET['activate-multi'] ) || is_network_admin() ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
return;
}
wp_safe_redirect( admin_url( 'index.php?page=aioseo-setup-wizard' ) );
exit;
}
/**
* Adds a dashboard page for our setup wizard.
*
* @since 4.0.0
*
* @return void
*/
public function addDashboardPage() {
add_dashboard_page( '', '', aioseo()->admin->getPageRequiredCapability( 'aioseo-setup-wizard' ), 'aioseo-setup-wizard', '' );
}
/**
* Hide the dashboard page from the menu.
*
* @since 4.1.5
*
* @return void
*/
public function hideDashboardPageFromMenu() {
remove_submenu_page( 'index.php', 'aioseo-setup-wizard' );
}
/**
* Checks to see if we should load the setup wizard.
*
* @since 4.0.0
*
* @return void
*/
public function maybeLoadOnboardingWizard() {
// Don't load the interface if doing an ajax call.
if ( wp_doing_ajax() || wp_doing_cron() ) {
return;
}
// Check for wizard-specific parameter
// Allow plugins to disable the setup wizard
// Check if current user is allowed to save settings.
if (
// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
! isset( $_GET['page'] ) ||
'aioseo-setup-wizard' !== sanitize_text_field( wp_unslash( $_GET['page'] ) ) ||
// phpcs:enable
! current_user_can( aioseo()->admin->getPageRequiredCapability( 'aioseo-setup-wizard' ) )
) {
return;
}
set_current_screen();
// Remove an action in the Gutenberg plugin ( not core Gutenberg ) which throws an error.
remove_action( 'admin_print_styles', 'gutenberg_block_editor_admin_print_styles' );
// If we are redirecting, clear the transient so it only happens once.
aioseo()->core->cache->delete( 'activation_redirect' );
$this->loadOnboardingWizard();
}
/**
* Load the Onboarding Wizard template.
*
* @since 4.0.0
*
* @return void
*/
private function loadOnboardingWizard() {
$this->enqueueScripts();
$this->setupWizardHeader();
$this->setupWizardContent();
$this->setupWizardFooter();
exit;
}
/**
* Enqueue's scripts for the setup wizard.
*
* @since 4.0.0
*
* @return void
*/
public function enqueueScripts() {
// We don't want any plugin adding notices to our screens. Let's clear them out here.
remove_all_actions( 'admin_notices' );
remove_all_actions( 'network_admin_notices' );
remove_all_actions( 'all_admin_notices' );
aioseo()->core->assets->load( 'src/vue/standalone/setup-wizard/main.js', [], aioseo()->helpers->getVueData( 'setup-wizard' ) );
aioseo()->main->enqueueTranslations();
wp_enqueue_style( 'common' );
wp_enqueue_media();
}
/**
* Outputs the simplified header used for the Onboarding Wizard.
*
* @since 4.0.0
*
* @return void
*/
public function setupWizardHeader() {
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta name="viewport" content="width=device-width"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>
<?php
// Translators: 1 - The plugin name ("All in One SEO").
echo sprintf( esc_html__( '%1$s › Onboarding Wizard', 'all-in-one-seo-pack' ), esc_html( AIOSEO_PLUGIN_SHORT_NAME ) );
?>
</title>
</head>
<body class="aioseo-setup-wizard">
<?php
}
/**
* Outputs the content of the current step.
*
* @since 4.0.0
*
* @return void
*/
public function setupWizardContent() {
echo '<div id="aioseo-app">';
aioseo()->templates->getTemplate( 'admin/settings-page.php' );
echo '</div>';
}
/**
* Outputs the simplified footer used for the Onboarding Wizard.
*
* @since 4.0.0
*
* @return void
*/
public function setupWizardFooter() {
?>
<?php
wp_print_scripts( 'aioseo-vendors' );
wp_print_scripts( 'aioseo-common' );
wp_print_scripts( 'aioseo-setup-wizard-script' );
do_action( 'admin_footer', '' );
do_action( 'admin_print_footer_scripts' );
// do_action( 'customize_controls_print_footer_scripts' );
?>
</body>
</html>
<?php
}
/**
* Check whether or not the Setup Wizard is completed.
*
* @since 4.2.0
*
* @return boolean Whether or not the Setup Wizard is completed.
*/
public function isCompleted() {
$wizard = (string) aioseo()->internalOptions->internal->wizard;
$wizard = json_decode( $wizard );
if ( ! $wizard ) {
return false;
}
$totalStageCount = count( $wizard->stages );
$currentStageCount = array_search( $wizard->currentStage, $wizard->stages, true );
// If not found, let's assume it's completed.
if ( false === $currentStageCount ) {
return true;
}
return $currentStageCount + 1 === $totalStageCount;
}
/**
* Get the next stage of the wizard.
*
* @since 4.6.2
*
* @return string The next stage or empty.
*/
public function getNextStage() {
$wizard = (string) aioseo()->internalOptions->internal->wizard;
$wizard = json_decode( $wizard );
if ( ! $wizard ) {
return '';
}
// Default to success.
$nextStage = 'success';
// Get the next stage of the wizard.
$currentStageIndex = array_search( $wizard->currentStage, $wizard->stages, true );
if ( ! empty( $wizard->stages[ $currentStageIndex + 1 ] ) ) {
$nextStage = $wizard->stages[ $currentStageIndex + 1 ];
}
return $nextStage;
}
} Standalone/Standalone.php 0000666 00000005446 15113050717 0011452 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Pro\Standalone as ProStandalone;
/**
* Registers the standalone components.
*
* @since 4.2.0
*/
class Standalone {
/**
* HeadlineAnalyzer class instance.
*
* @since 4.2.7
*
* @var HeadlineAnalyzer
*/
public $headlineAnalyzer = null;
/**
* FlyoutMenu class instance.
*
* @since 4.2.7
*
* @var FlyoutMenu
*/
public $flyoutMenu = null;
/**
* SeoPreview class instance.
*
* @since 4.2.8
*
* @var SeoPreview
*/
public $seoPreview = null;
/**
* SetupWizard class instance.
*
* @since 4.2.7
*
* @var SetupWizard
*/
public $setupWizard = null;
/**
* PrimaryTerm class instance.
*
* @since 4.3.6
*
* @var PrimaryTerm
*/
public $primaryTerm = null;
/**
* UserProfileTab class instance.
*
* @since 4.5.4
*
* @var UserProfileTab
*/
public $userProfileTab = null;
/**
* BuddyPress class instance.
*
* @since 4.7.6
*
* @var BuddyPress\BuddyPress
*/
public $buddyPress = null;
/**
* BbPress class instance.
*
* @since 4.8.1
*
* @var BbPress\BbPress
*/
public $bbPress = null;
/**
* List of page builder integration class instances.
*
* @since 4.2.7
*
* @var array[Object]
*/
public $pageBuilderIntegrations = [];
/**
* List of block class instances.
*
* @since 4.2.7
*
* @var array[Object]
*/
public $standaloneBlocks = [];
/**
* Class constructor.
*
* @since 4.2.0
*/
public function __construct() {
$this->headlineAnalyzer = new HeadlineAnalyzer();
$this->flyoutMenu = new FlyoutMenu();
$this->seoPreview = new SeoPreview();
$this->setupWizard = new SetupWizard();
$this->primaryTerm = aioseo()->pro ? new ProStandalone\PrimaryTerm() : new PrimaryTerm();
$this->userProfileTab = new UserProfileTab();
$this->buddyPress = aioseo()->pro ? new ProStandalone\BuddyPress\BuddyPress() : new BuddyPress\BuddyPress();
$this->bbPress = aioseo()->pro ? new ProStandalone\BbPress\BbPress() : new BbPress\BbPress();
aioseo()->pro ? new ProStandalone\DetailsColumn() : new DetailsColumn();
new AdminBarNoindexWarning();
new LimitModifiedDate();
new Notifications();
new PublishPanel();
new WpCode();
$this->pageBuilderIntegrations = [
'elementor' => new PageBuilders\Elementor(),
'divi' => new PageBuilders\Divi(),
'seedprod' => new PageBuilders\SeedProd(),
'wpbakery' => new PageBuilders\WPBakery(),
'avada' => new PageBuilders\Avada(),
'siteorigin' => new PageBuilders\SiteOrigin(),
'thrive' => new PageBuilders\ThriveArchitect()
];
$this->standaloneBlocks = [
'tocBlock' => new Blocks\TableOfContents(),
'faqBlock' => new Blocks\FaqPage()
];
}
} Standalone/LimitModifiedDate.php 0000666 00000016355 15113050717 0012700 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Limit Modified Date class.
*
* @since 4.1.8
*/
class LimitModifiedDate {
/**
* Class constructor.
*
* @since 4.1.8
*
* @return void
*/
public function __construct() {
if ( apply_filters( 'aioseo_last_modified_date_disable', false ) ) {
return;
}
// Reset modified date when the post is updated.
add_filter( 'wp_insert_post_data', [ $this, 'resetModifiedDate' ], 99999, 2 );
add_filter( 'wp_insert_attachment_data', [ $this, 'resetModifiedDate' ], 99999, 2 );
add_action( 'woocommerce_before_product_object_save', [ $this, 'limitWooCommerceModifiedDate' ] );
add_action( 'rest_api_init', [ $this, 'registerRestHooks' ] );
if ( ! is_admin() ) {
return;
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScripts' ], 20 );
add_action( 'post_submitbox_misc_actions', [ $this, 'classicEditorField' ] );
}
/**
* Register the REST API hooks.
*
* @since 4.1.8
*
* @return void
*/
public function registerRestHooks() {
// Prevent REST API from dropping limit modified date value before updating the post.
foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {
add_filter( "rest_pre_insert_$postType", [ $this, 'addLimitModifiedDateValue' ], 10, 2 );
}
}
/**
* Enqueues the scripts for the Limited Modified Date functionality.
*
* @since 4.1.8
*
* @return void
*/
public function enqueueScripts() {
if ( ! $this->isAllowed() || ! aioseo()->helpers->isScreenBase( 'post' ) ) {
return;
}
// Only enqueue this script if the post-settings-metabox is already enqueued.
if ( wp_script_is( 'aioseo/js/src/vue/standalone/post-settings/main.js', 'enqueued' ) ) {
aioseo()->core->assets->load( 'src/vue/standalone/limit-modified-date/main.js' );
}
}
/**
* Adds the Limit Modified Date field to the post object to prevent it from being dropped.
*
* @since 4.1.8
*
* @param object $preparedPost The post data.
* @param \WP_REST_Request $restRequest The request.
* @return object The modified post data.
*/
public function addLimitModifiedDateValue( $preparedPost, $restRequest = null ) {
if ( 'PUT' !== $restRequest->get_method() ) {
return $preparedPost;
}
$params = $restRequest->get_json_params();
if ( empty( $params ) || ! isset( $params['aioseo_limit_modified_date'] ) ) {
return $preparedPost;
}
$preparedPost->aioseo_limit_modified_date = $params['aioseo_limit_modified_date'];
return $preparedPost;
}
/**
* Resets the modified date when a post is updated if the Limit Modified Date option is enabled.
*
* @since 4.1.8
*
* @param array $data An array of slashed, sanitized, and processed post data.
* @param array $postArray An array of sanitized (and slashed) but otherwise unmodified post data.
* @return array The modified sanitized post data.
*/
public function resetModifiedDate( $data, $postArray = [] ) {
// If the ID isn't set, a new post is being inserted.
if ( ! isset( $postArray['ID'] ) ) {
return $data;
}
static $shouldReset = false;
// Handle the REST API request from the Block Editor.
if ( aioseo()->helpers->isRestApiRequest() ) {
// If the value isn't set, then the value wasn't changed in the editor, and we can grab it from the post.
if ( ! isset( $postArray['aioseo_limit_modified_date'] ) ) {
$aioseoPost = Models\Post::getPost( $postArray['ID'] );
if ( $aioseoPost->exists() && $aioseoPost->limit_modified_date ) {
$shouldReset = true;
}
} else {
if ( $postArray['aioseo_limit_modified_date'] ) {
$shouldReset = true;
}
}
}
// Handle the POST request.
if ( isset( $postArray['aioseo-post-settings'] ) ) {
$aioseoData = json_decode( stripslashes( $postArray['aioseo-post-settings'] ) );
if ( ! empty( $aioseoData->limit_modified_date ) ) {
$shouldReset = true;
}
}
// Handle post revision.
if ( ! empty( $GLOBALS['action'] ) && in_array( $GLOBALS['action'], [ 'restore', 'inline-save' ], true ) ) {
$aioseoPost = Models\Post::getPost( $postArray['ID'] );
if ( $aioseoPost->exists() && $aioseoPost->limit_modified_date ) {
$shouldReset = true;
}
}
foreach ( aioseo()->standalone->pageBuilderIntegrations as $pageBuilder ) {
if ( $pageBuilder->isBuiltWith( $postArray['ID'] ) && $pageBuilder->limitModifiedDate( $postArray['ID'] ) ) {
$shouldReset = true;
break;
}
}
if ( $shouldReset && isset( $postArray['post_modified'], $postArray['post_modified_gmt'] ) ) {
$originalPost = get_post( $postArray['ID'] );
$data['post_modified'] = $originalPost->post_modified;
$data['post_modified_gmt'] = $originalPost->post_modified_gmt;
}
return $data;
}
/**
* Limits the modified date for WooCommerce products.
*
* @since 4.8.1
*
* @param \WC_Product $product The WooCommerce product.
* @return void
*/
public function limitWooCommerceModifiedDate( $product ) {
if ( ! isset( $_POST['PostSettingsNonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['PostSettingsNonce'] ) ), 'aioseoPostSettingsNonce' ) ) {
return;
}
if ( ! isset( $_POST['aioseo-post-settings'] ) ) {
return;
}
$aioseoData = json_decode( sanitize_text_field( wp_unslash( ( $_POST['aioseo-post-settings'] ) ) ) );
if ( empty( $aioseoData ) || empty( $aioseoData->limit_modified_date ) ) {
return;
}
$product->set_date_modified( get_post_field( 'post_modified', $product->get_id() ) );
}
/**
* Add the checkbox in the Classic Editor.
*
* @since 4.1.8
*
* @param \WP_Post $post The post object.
* @return void
*/
public function classicEditorField( $post ) {
if ( ! $this->isAllowed( $post->post_type ) ) {
return;
}
?>
<div class="misc-pub-section">
<div id="aioseo-limit-modified-date"></div>
</div>
<?php
}
/**
* Check if the Limit Modified Date functionality is allowed to run.
*
* @since 4.1.8
*
* @param string $postType The current post type.
* @return bool Whether the functionality is allowed.
*/
private function isAllowed( $postType = '' ) {
if ( empty( $postType ) ) {
$postType = get_post_type();
}
if ( class_exists( 'Limit_Modified_Date', false ) ) {
return false;
}
if ( ! $this->isAllowedPostType( $postType ) ) {
return false;
}
if ( ! aioseo()->access->hasCapability( 'aioseo_page_general_settings' ) ) {
return false;
}
return true;
}
/**
* Check if the given post type is allowed to limit the modified date.
*
* @since 4.1.8
*
* @param string $postType The post type name.
* @return bool Whether the post type is allowed.
*/
private function isAllowedPostType( $postType ) {
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
$postTypes = aioseo()->helpers->getPublicPostTypes( true );
$postTypes = apply_filters( 'aioseo_limit_modified_date_post_types', $postTypes );
if ( ! in_array( $postType, $postTypes, true ) ) {
return false;
}
if ( ! $dynamicOptions->searchAppearance->postTypes->has( $postType ) || ! $dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox ) {
return false;
}
return true;
}
} Standalone/BbPress/Component.php 0000666 00000005100 15113050717 0012647 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\BbPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* bbPress Component class.
*
* @since 4.8.1
*/
class Component {
/**
* The current component template type.
*
* @since 4.8.1
*
* @var string|null
*/
public $templateType = null;
/**
* The topic single page data.
*
* @since 4.8.1
*
* @var array
*/
public $topic = [];
/**
* Class constructor.
*
* @since 4.8.1
*/
public function __construct() {
if ( is_admin() ) {
return;
}
$this->setTemplateType();
$this->setTopic();
}
/**
* Sets the template type.
*
* @since 4.8.1
*
* @return void
*/
private function setTemplateType() {
if ( function_exists( 'bbp_is_single_topic' ) && bbp_is_single_topic() ) {
$this->templateType = 'bbp-topic_single';
}
}
/**
* Sets the topic data.
*
* @since 4.8.1
*
* @return void
*/
private function setTopic() {
if ( 'bbp-topic_single' !== $this->templateType ) {
return;
}
if (
! function_exists( 'bbpress' ) ||
! function_exists( 'bbp_has_replies' ) ||
! bbp_has_replies()
) {
return;
}
$replyQuery = bbpress()->reply_query ?? null;
$replies = $replyQuery->posts ?? [];
$mainTopic = is_array( $replies ) && ! empty( $replies ) ? array_shift( $replies ) : null;
if ( $mainTopic instanceof \WP_Post ) {
$this->topic = [
'title' => $mainTopic->post_title,
'content' => $mainTopic->post_content,
'date' => $mainTopic->post_date,
'author' => get_the_author_meta( 'display_name', $mainTopic->post_author ),
];
$comments = [];
if ( ! empty( $replies ) ) {
foreach ( $replies as $reply ) {
if ( ! $reply instanceof \WP_Post ) {
continue;
}
$comments[ $reply->ID ] = [
'content' => $reply->post_content,
'date_recorded' => $reply->post_date,
'user_fullname' => get_the_author_meta( 'display_name', $reply->post_author ),
];
if ( ! empty( $reply->reply_to ) ) {
$comments[ $reply->reply_to ]['children'][] = $comments[ $reply->ID ];
unset( $comments[ $reply->ID ] );
}
}
$this->topic['comment'] = array_values( $comments );
}
return;
}
$this->resetComponent();
}
/**
* Resets some of the component properties.
*
* @since 4.8.1
*
* @return void
*/
private function resetComponent() {
$this->templateType = null;
}
/**
* Determines the schema type for the current component.
*
* @since 4.8.1
*
* @return void
*/
public function determineSchemaGraphsAndContext() {
}
} Standalone/BbPress/BbPress.php 0000666 00000002273 15113050717 0012255 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\BbPress;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the bbPress integration with AIOSEO.
*
* @since 4.8.1
*/
class BbPress {
/**
* Instance of the Component class.
*
* @since 4.8.1
*
* @var Component
*/
public $component;
/**
* Class constructor.
*
* @since 4.8.1
*/
public function __construct() {
if (
aioseo()->helpers->isAjaxCronRestRequest() ||
! aioseo()->helpers->isPluginActive( 'bbpress' )
) {
return;
}
// Hook into `plugins_loaded` to ensure bbPress has loaded some necessary functions.
add_action( 'plugins_loaded', [ $this, 'maybeLoad' ], 20 );
}
/**
* Hooked into `plugins_loaded` action hook.
*
* @since 4.8.1
*
* @return void
*/
public function maybeLoad() {
// If the bbPress version is below 2 we bail.
if ( ! function_exists( 'bbp_get_version' ) || version_compare( bbp_get_version(), '2', '<' ) ) {
return;
}
add_action( 'wp', [ $this, 'setComponent' ] );
}
/**
* Hooked into `wp` action hook.
*
* @since 4.8.1
*
* @return void
*/
public function setComponent() {
$this->component = new Component();
}
} Standalone/FlyoutMenu.php 0000666 00000003405 15113050717 0011462 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the flyout menu.
*
* @since 4.2.0
*/
class FlyoutMenu {
/**
* Class constructor.
*
* @since 4.2.0
*/
public function __construct() {
if (
! is_admin() ||
wp_doing_ajax() ||
wp_doing_cron() ||
! $this->isEnabled()
) {
return;
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ], 11 );
add_filter( 'admin_body_class', [ $this, 'addBodyClass' ] );
}
/**
* Enqueues the required assets.
*
* @since 4.2.0
*
* @return void
*/
public function enqueueAssets() {
if ( ! $this->shouldEnqueue() ) {
return;
}
aioseo()->core->assets->load( 'src/vue/standalone/flyout-menu/main.js' );
}
/**
* Filters the CSS classes for the body tag in the admin.
*
* @since 4.2.0
*
* @param string $classes Space-separated list of CSS classes.
* @return string Space-separated list of CSS classes.
*/
public function addBodyClass( $classes ) {
if ( $this->shouldEnqueue() ) {
// This adds a bottom margin to our menu so that we push the footer down and prevent the flyout menu from overlapping the "Save Changes" button.
$classes .= ' aioseo-flyout-menu-enabled ';
}
return $classes;
}
/**
* Checks whether the flyout menu script should be enqueued.
*
* @since 4.2.0
*
* @return bool Whether the flyout menu script should be enqueued.
*/
private function shouldEnqueue() {
return aioseo()->admin->isAioseoScreen();
}
/**
* Checks whether the flyout menu is enabled.
*
* @since 4.2.0
*
* @return bool Whether the flyout menu is enabled.
*/
public function isEnabled() {
return apply_filters( 'aioseo_flyout_menu_enable', true );
}
} Standalone/PrimaryTerm.php 0000666 00000002402 15113050717 0011622 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the Primary Term feature.
*
* @since 4.3.6
*/
class PrimaryTerm {
/**
* Class constructor.
*
* @since 4.3.6
*/
public function __construct() {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_doing_ajax() || wp_doing_cron() || ! is_admin() ) {
return;
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ] );
}
/**
* Enqueues the JS/CSS for the on page/posts settings.
*
* @since 4.3.6
*
* @return void
*/
public function enqueueAssets() {
if ( ! aioseo()->helpers->isScreenBase( 'post' ) ) {
return;
}
aioseo()->core->assets->load( 'src/vue/standalone/primary-term/main.js', [], aioseo()->helpers->getVueData( 'post' ) );
}
/**
* Returns the primary term for the given taxonomy name.
*
* @since 4.3.6
*
* @param int $postId The post ID.
* @param string $taxonomyName The taxonomy name.
* @return \WP_Term|false The term or false.
*/
public function getPrimaryTerm( $postId, $taxonomyName ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return false;
}
} Standalone/HeadlineAnalyzer.php 0000666 00000040326 15113050717 0012575 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the headline analysis.
*
* @since 4.1.2
*/
class HeadlineAnalyzer {
/**
* Class constructor.
*
* @since 4.1.2
*/
public function __construct() {
if ( ! is_admin() || wp_doing_cron() ) {
return;
}
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue' ] );
if ( ! aioseo()->options->advanced->headlineAnalyzer ) {
return;
}
add_filter( 'monsterinsights_headline_analyzer_enabled', '__return_false' );
add_filter( 'exactmetrics_headline_analyzer_enabled', '__return_false' );
}
/**
* Enqueues the headline analyzer.
*
* @since 4.1.2
*
* @return void
*/
public function enqueue() {
if (
! aioseo()->helpers->isScreenBase( 'post' ) ||
! aioseo()->access->hasCapability( 'aioseo_page_analysis' )
) {
return;
}
if ( ! aioseo()->options->advanced->headlineAnalyzer ) {
return;
}
$path = '/vendor/jwhennessey/phpinsight/autoload.php';
if ( ! aioseo()->core->fs->exists( AIOSEO_DIR . $path ) ) {
return;
}
require AIOSEO_DIR . $path;
aioseo()->core->assets->load( 'src/vue/standalone/headline-analyzer/main.js' );
}
/**
* Returns the result of the analsyis.
*
* @since 4.1.2
*
* @param string $title The title.
* @return array The result.
*/
public function getResult( $title ) {
$result = $this->getHeadlineScore( html_entity_decode( $title ) );
return [
'result' => $result,
'analysed' => ! $result->err,
'sentence' => ucwords( wp_unslash( sanitize_text_field( $title ) ) ),
'score' => ! empty( $result->score ) ? $result->score : 0
];
}
/**
* Returns the score.
*
* @since 4.1.2
*
* @param string $title The title.
* @return \stdClass The result.
*/
public function getHeadlineScore( $title ) {
$result = new \stdClass();
$result->originalExplodedHeadline = explode( ' ', wp_unslash( $title ) );
// Strip useless characters and whitespace.
$title = preg_replace( '/[^A-Za-z0-9 ]/', '', (string) $title );
$title = preg_replace( '!\s+!', ' ', (string) $title );
$title = strtolower( $title );
$result->input = $title;
// If the headline is invalid, return an error.
if ( ! $title || ' ' === $title || trim( $title ) === '' ) {
$result->err = true;
$result->msg = 'The headline is invalid.';
return $result;
}
$totalScore = 0;
$explodedHeadline = explode( ' ', $title );
$result->explodedHeadline = $explodedHeadline;
$result->err = false;
// The optimal length is 55 characters.
$result->length = strlen( str_replace( ' ', '', $title ) );
$totalScore = $totalScore + 3;
//phpcs:disable Squiz.ControlStructures.ControlSignature
if ( $result->length <= 19 ) { $totalScore += 5; }
elseif ( $result->length >= 20 && $result->length <= 34 ) { $totalScore += 8; }
elseif ( $result->length >= 35 && $result->length <= 66 ) { $totalScore += 11; }
elseif ( $result->length >= 67 && $result->length <= 79 ) { $totalScore += 8; }
elseif ( $result->length >= 80 ) { $totalScore += 5; }
// The average headline is 6-7 words long.
$result->wordCount = count( $explodedHeadline );
$totalScore = $totalScore + 3;
if ( 0 === $result->wordCount ) { $totalScore = 0; }
elseif ( $result->wordCount >= 2 && $result->wordCount <= 4 ) { $totalScore += 5; }
elseif ( $result->wordCount >= 5 && $result->wordCount <= 9 ) { $totalScore += 11; }
elseif ( $result->wordCount >= 10 && $result->wordCount <= 11 ) { $totalScore += 8; }
elseif ( $result->wordCount >= 12 ) { $totalScore += 5; }
// Check for power words, emotional words, etc.
$result->powerWords = $this->matchWords( $result->input, $result->explodedHeadline, $this->powerWords() );
$result->powerWordsPercentage = count( $result->powerWords ) / $result->wordCount;
$result->emotionWords = $this->matchWords( $result->input, $result->explodedHeadline, $this->emotionPowerWords() );
$result->emotionalWordsPercentage = count( $result->emotionWords ) / $result->wordCount;
$result->commonWords = $this->matchWords( $result->input, $result->explodedHeadline, $this->commonWords() );
$result->commonWordsPercentage = count( $result->commonWords ) / $result->wordCount;
$result->uncommonWords = $this->matchWords( $result->input, $result->explodedHeadline, $this->uncommonWords() );
$result->uncommonWordsPercentage = count( $result->uncommonWords ) / $result->wordCount;
$result->detectedWordTypes = [];
if ( $result->emotionalWordsPercentage < 0.1 ) {
$result->detectedWordTypes[] = 'emotion';
} else {
$totalScore = $totalScore + 15;
}
if ( $result->commonWordsPercentage < 0.2 ) {
$result->detectedWordTypes[] = 'common';
} else {
$totalScore = $totalScore + 11;
}
if ( $result->uncommonWordsPercentage < 0.1 ) {
$result->detectedWordTypes[] = 'uncommon';
} else {
$totalScore = $totalScore + 15;
}
if ( count( $result->powerWords ) < 1 ) {
$result->detectedWordTypes[] = 'power';
} else {
$totalScore = $totalScore + 19;
}
if (
$result->emotionalWordsPercentage >= 0.1 &&
$result->commonWordsPercentage >= 0.2 &&
$result->uncommonWordsPercentage >= 0.1 &&
count( $result->powerWords ) >= 1
) {
$totalScore = $totalScore + 3;
}
$sentiment = new \PHPInsight\Sentiment();
$sentimentClass = $sentiment->categorise( $title );
$result->sentiment = $sentimentClass;
$totalScore = $totalScore + ( 'pos' === $result->sentiment ? 10 : ( 'neg' === $result->sentiment ? 10 : 7 ) );
$headlineTypes = [];
if ( strpos( $title, 'how to' ) !== false || strpos( $title, 'howto' ) !== false ) {
$headlineTypes[] = __( 'How-To', 'all-in-one-seo-pack' );
$totalScore = $totalScore + 7;
}
$listWords = array_intersect( $explodedHeadline, $this->numericalIndicators() );
if ( preg_match( '~[0-9]+~', (string) $title ) || ! empty( $listWords ) ) {
$headlineTypes[] = __( 'List', 'all-in-one-seo-pack' );
$totalScore = $totalScore + 7;
}
if ( in_array( $explodedHeadline[0], $this->primaryQuestionIndicators(), true ) ) {
if ( in_array( $explodedHeadline[1], $this->secondaryQuestionIndicators(), true ) ) {
$headlineTypes[] = __( 'Question', 'all-in-one-seo-pack' );
$totalScore = $totalScore + 7;
}
}
if ( empty( $headlineTypes ) ) {
$headlineTypes[] = __( 'General', 'all-in-one-seo-pack' );
$totalScore = $totalScore + 5;
}
$result->headlineTypes = $headlineTypes;
$result->score = $totalScore >= 93 ? 93 : $totalScore;
return $result;
}
/**
* Tries to find matches for power words, emotional words, etc. in the headline.
*
* @since 4.1.2
*
* @param string $headline The headline.
* @param array $explodedHeadline The exploded headline.
* @param array $words The words to match.
* @return array The matches that were found.
*/
public function matchWords( $headline, $explodedHeadline, $words ) {
$foundMatches = [];
foreach ( $words as $word ) {
$strippedWord = preg_replace( '/[^A-Za-z0-9 ]/', '', (string) $word );
// Check if word is a phrase.
if ( strpos( $word, ' ' ) !== false ) {
if ( strpos( $headline, $strippedWord ) !== false ) {
$foundMatches[] = $word;
}
continue;
}
// Check if it is a single word.
if ( in_array( $strippedWord, $explodedHeadline, true ) ) {
$foundMatches[] = $word;
}
}
return $foundMatches;
}
/**
* Returns a list of numerical indicators.
*
* @since 4.1.2
*
* @return array The list of numerical indicators.
*/
private function numericalIndicators() {
return [
'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'eleven', 'twelve', 'thirt', 'fift', 'hundred', 'thousand' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
];
}
/**
* Returns a list of primary question indicators.
*
* @since 4.1.2
*
* @return array The list of primary question indicators.
*/
private function primaryQuestionIndicators() {
return [
'where', 'when', 'how', 'what', 'have', 'has', 'does', 'do', 'can', 'are', 'will' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
];
}
/**
* Returns a list of secondary question indicators.
*
* @since 4.1.2
*
* @return array The list of secondary question indicators.
*/
private function secondaryQuestionIndicators() {
return [
'you', 'they', 'he', 'she', 'your', 'it', 'they', 'my', 'have', 'has', 'does', 'do', 'can', 'are', 'will' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
];
}
/**
* Returns a list of power words.
*
* @since 4.1.2
*
* @return array The list of power words.
*/
private function powerWords() {
return [
'great', 'free', 'focus', 'remarkable', 'confidential', 'sale', 'wanted', 'obsession', 'sizable', 'new', 'absolutely lowest', 'surging', 'wonderful', 'professional', 'interesting', 'revisited', 'delivered', 'guaranteed', 'challenge', 'unique', 'secrets', 'special', 'lifetime', 'bargain', 'scarce', 'tested', 'highest', 'hurry', 'alert famous', 'improved', 'expert', 'daring', 'strong', 'immediately', 'advice', 'pioneering', 'unusual', 'limited', 'the truth about', 'destiny', 'outstanding', 'simplistic', 'compare', 'unsurpassed', 'energy', 'powerful', 'colorful', 'genuine', 'instructive', 'big', 'affordable', 'informative', 'liberal', 'popular', 'ultimate', 'mainstream', 'rare', 'exclusive', 'willpower', 'complete', 'edge', 'valuable', 'attractive', 'last chance', 'superior', 'how to', 'easily', 'exploit', 'unparalleled', 'endorsed', 'approved', 'quality', 'fascinating', 'unlimited', 'competitive', 'gigantic', 'compromise', 'discount', 'full', 'love', 'odd', 'fundamentals', 'mammoth', 'lavishly', 'bottom line', 'under priced', 'innovative', 'reliable', 'zinger', 'suddenly', 'it\'s here', 'terrific', 'simplified', 'perspective', 'just arrived', 'breakthrough', 'tremendous', 'launching', 'sure fire', 'emerging', 'helpful', 'skill', 'soar', 'profitable', 'special offer', 'reduced', 'beautiful', 'sampler', 'technology', 'better', 'crammed', 'noted', 'selected', 'shrewd', 'growth', 'luxury', 'sturdy', 'enormous', 'promising', 'unconditional', 'wealth', 'spotlight', 'astonishing', 'timely', 'successful', 'useful', 'imagination', 'bonanza', 'opportunities', 'survival', 'greatest', 'security', 'last minute', 'largest', 'high tech', 'refundable', 'monumental', 'colossal', 'latest', 'quickly', 'startling', 'now', 'important', 'revolutionary', 'quick', 'unlock', 'urgent', 'miracle', 'easy', 'fortune', 'amazing', 'magic', 'direct', 'authentic', 'exciting', 'proven', 'simple', 'announcing', 'portfolio', 'reward', 'strange', 'huge gift', 'revealing', 'weird', 'value', 'introducing', 'sensational', 'surprise', 'insider', 'practical', 'excellent', 'delighted', 'download' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
];
}
/**
* Returns a list of common words.
*
* @since 4.1.2
*
* @return array The list of common words.
*/
private function commonWords() {
return [
'a', 'for', 'about', 'from', 'after', 'get', 'all', 'has', 'an', 'have', 'and', 'he', 'are', 'her', 'as', 'his', 'at', 'how', 'be', 'I', 'but', 'if', 'by', 'in', 'can', 'is', 'did', 'it', 'do', 'just', 'ever', 'like', 'll', 'these', 'me', 'they', 'most', 'things', 'my', 'this', 'no', 'to', 'not', 'up', 'of', 'was', 'on', 'what', 're', 'when', 'she', 'who', 'sould', 'why', 'so', 'will', 'that', 'with', 'the', 'you', 'their', 'your', 'there' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
];
}
/**
* Returns a list of uncommon words.
*
* @since 4.1.2
*
* @return array The list of uncommon words.
*/
private function uncommonWords() {
return [
'actually', 'happened', 'need', 'thing', 'awesome', 'heart', 'never', 'think', 'baby', 'here', 'new', 'time', 'beautiful', 'its', 'now', 'valentines', 'being', 'know', 'old', 'video', 'best', 'life', 'one', 'want', 'better', 'little', 'out', 'watch', 'boy', 'look', 'people', 'way', 'dog', 'love', 'photos', 'ways', 'down', 'made', 'really', 'world', 'facebook', 'make', 'reasons', 'year', 'first', 'makes', 'right', 'years', 'found', 'man', 'see', 'you’ll', 'girl', 'media', 'seen', 'good', 'mind', 'social', 'guy', 'more', 'something' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
];
}
/**
* Returns a list of emotional power words.
*
* @since 4.1.2
*
* @return array The list of emotional power words.
*/
private function emotionPowerWords() {
return [
'destroy', 'extra', 'in a', 'devastating', 'eye-opening', 'gift', 'in the world', 'devoted', 'fail', 'in the', 'faith', 'grateful', 'inexpensive', 'dirty', 'famous', 'disastrous', 'fantastic', 'greed', 'grit', 'insanely', 'disgusting', 'fearless', 'disinformation', 'feast', 'insidious', 'dollar', 'feeble', 'gullible', 'double', 'fire', 'hack', 'fleece', 'had enough', 'invasion', 'drowning', 'floundering', 'happy', 'ironclad', 'dumb', 'flush', 'hate', 'irresistibly', 'hazardous', 'is the', 'fool', 'is what happens when', 'fooled', 'helpless', 'it looks like a', 'embarrass', 'for the first time', 'help are the', 'jackpot', 'forbidden', 'hidden', 'jail', 'empower', 'force-fed', 'high', 'jaw-dropping', 'forgotten', 'jeopardy', 'energize', 'hoax', 'jubilant', 'foul', 'hope', 'killer', 'frantic', 'horrific', 'know it all', 'epic', 'how to make', 'evil', 'freebie', 'frenzy', 'hurricane', 'excited', 'fresh on the mind', 'frightening', 'hypnotic', 'lawsuit', 'frugal', 'illegal', 'fulfill', 'lick', 'explode', 'lies', 'exposed', 'gambling', 'like a normal', 'nightmare', 'results', 'line', 'no good', 'pound', 'loathsome', 'no questions asked', 'revenge', 'lonely', 'looks like a', 'obnoxious', 'preposterous', 'revolting', 'looming', 'priced', 'lost', 'prison', 'lowest', 'of the', 'privacy', 'rich', 'lunatic', 'off-limits', 'private', 'risky', 'lurking', 'offer', 'prize', 'ruthless', 'lust', 'official', 'luxurious', 'on the', 'profit', 'scary', 'lying', 'outlawed', 'protected', 'scream', 'searing', 'overcome', 'provocative', 'make you', 'painful', 'pummel', 'secure', 'pale', 'punish', 'marked down', 'panic', 'quadruple', 'seductively', 'massive', 'pay zero', 'seize', 'meltdown', 'payback', 'might look like a', 'peril', 'mind-blowing', 'shameless', 'minute', 'rave', 'shatter', 'piranha', 'reckoning', 'shellacking', 'mired', 'pitfall', 'reclaim', 'mistakes', 'plague', 'sick and tired', 'money', 'played', 'refugee', 'silly', 'money-grubbing', 'pluck', 'refund', 'moneyback', 'plummet', 'plunge', 'murder', 'pointless', 'sinful', 'myths', 'poor', 'remarkably', 'six-figure', 'never again', 'research', 'surrender', 'to the', 'varify', 'skyrocket', 'toxic', 'vibrant', 'slaughter', 'swindle', 'trap', 'victim', 'sleazy', 'taboo', 'treasure', 'victory', 'smash', 'tailspin', 'vindication', 'smug', 'tank', 'triple', 'viral', 'smuggled', 'tantalizing', 'triumph', 'volatile', 'sniveling', 'targeted', 'truth', 'vulnerable', 'snob', 'tawdry', 'try before you buy', 'tech', 'turn the tables', 'wanton', 'soaring', 'warning', 'teetering', 'unauthorized', 'spectacular', 'temporary fix', 'unbelievably', 'spine', 'tempting', 'uncommonly', 'what happened', 'spirit', 'what happens when', 'terror', 'under', 'what happens', 'staggering', 'underhanded', 'what this', 'that will make you', 'undo","when you see', 'that will make', 'unexpected', 'when you', 'strangle', 'that will', 'whip', 'the best', 'whopping', 'stuck up', 'the ranking of', 'wicked', 'stunning', 'the most', 'will make you', 'stupid', 'the reason why is', 'unscrupulous', 'thing ive ever seen', 'withheld', 'this is the', 'this is what happens', 'unusually', 'wondrous', 'this is what', 'uplifting', 'worry', 'sure', 'this is', 'wounded', 'surge', 'thrilled', 'you need to know', 'thrilling', 'valor', 'you need to', 'you see what', 'surprising', 'tired', 'you see', 'surprisingly', 'to be', 'vaporize' // phpcs:ignore Generic.Files.LineLength.MaxExceeded, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
];
}
} Standalone/Blocks/TableOfContents.php 0000666 00000000616 15113050717 0013623 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\Blocks;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Table of Contents Block.
*
* @since 4.2.3
*/
class TableOfContents extends Blocks {
/**
* Register the block.
*
* @since 4.2.3
*
* @return void
*/
public function register() {
aioseo()->blocks->registerBlock( 'aioseo/table-of-contents' );
}
} Standalone/Blocks/FaqPage.php 0000666 00000001075 15113050717 0012075 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\Blocks;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* FaqPage Block.
*
* @since 4.2.3
*/
class FaqPage extends Blocks {
/**
* Register the block.
*
* @since 4.2.3
*
* @return void
*/
public function register() {
aioseo()->blocks->registerBlock( 'aioseo/faq',
[
'render_callback' => function( $attributes, $content ) {
if ( isset( $attributes['hidden'] ) && true === $attributes['hidden'] ) {
return '';
}
return $content;
},
]
);
}
} Standalone/Blocks/Blocks.php 0000666 00000001166 15113050717 0012007 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\Blocks;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Loads core classes.
*
* @since 4.2.3
*/
abstract class Blocks {
/**
* Class constructor.
*
* @since 4.2.3
*/
public function __construct() {
add_action( 'init', [ $this, 'init' ] );
}
/**
* Initializes our blocks.
*
* @since 4.2.3
*
* @return void
*/
public function init() {
$this->register();
}
/**
* Registers the block. This is a wrapper to be extended in the child class.
*
* @since 4.2.3
*
* @return void
*/
public function register() {}
} Standalone/PublishPanel.php 0000666 00000001365 15113050717 0011744 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Handles the Publish Panel in the Block Editor.
*
* @since 4.2.0
*/
class PublishPanel {
/**
* Class constructor.
*
* @since 4.2.0
*/
public function __construct() {
if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
return;
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScript' ] );
}
/**
* Enqueues the script.
*
* @since 4.2.0
*
* @return void
*/
public function enqueueScript() {
if ( ! aioseo()->helpers->isScreenBase( 'post' ) ) {
return;
}
aioseo()->core->assets->load( 'src/vue/standalone/publish-panel/main.js' );
}
} Standalone/DetailsColumn.php 0000666 00000020310 15113050717 0012110 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Handles the AIOSEO Details post column.
*
* @since 4.2.0
*/
class DetailsColumn {
/**
* The slug for the script.
*
* @since 4.2.0
*
* @var string
*/
protected $scriptSlug = 'src/vue/standalone/posts-table/main.js';
/**
* Class constructor.
*
* @since 4.2.0
*/
public function __construct() {
if ( wp_doing_ajax() ) {
add_action( 'init', [ $this, 'addPostColumnsAjax' ], 1 );
}
if ( ! is_admin() || wp_doing_cron() ) {
return;
}
add_action( 'current_screen', [ $this, 'registerColumnHooks' ], 1 );
}
/**
* Adds the columns to the page/post types.
*
* @since 4.0.0
*
* @return void
*/
public function registerColumnHooks() {
$screen = aioseo()->helpers->getCurrentScreen();
if ( empty( $screen->base ) || empty( $screen->post_type ) ) {
return;
}
if ( ! $this->shouldRegisterColumn( $screen->base, $screen->post_type ) ) {
return;
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScripts' ] );
if ( 'product' === $screen->post_type ) {
add_filter( 'manage_edit-product_columns', [ $this, 'addColumn' ] );
add_action( 'manage_posts_custom_column', [ $this, 'renderColumn' ], 10, 2 );
return;
}
if ( 'attachment' === $screen->post_type ) {
$enabled = apply_filters( 'aioseo_image_seo_media_columns', true );
if ( ! $enabled ) {
return;
}
add_filter( 'manage_media_columns', [ $this, 'addColumn' ] );
add_action( 'manage_media_custom_column', [ $this, 'renderColumn' ], 10, 2 );
return;
}
add_filter( "manage_edit-{$screen->post_type}_columns", [ $this, 'addColumn' ] );
add_action( "manage_{$screen->post_type}_posts_custom_column", [ $this, 'renderColumn' ], 10, 2 );
}
/**
* Registers our post columns after a post has been quick-edited.
*
* @since 4.2.3
*
* @return void
*/
public function addPostColumnsAjax() {
if (
! isset( $_POST['_inline_edit'], $_POST['post_ID'], $_POST['aioseo-has-details-column'] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_inline_edit'] ) ), 'inlineeditnonce' )
) {
return;
}
$postId = (int) $_POST['post_ID'];
if ( ! $postId ) {
return;
}
$post = get_post( $postId );
$postType = $post->post_type;
add_filter( "manage_edit-{$postType}_columns", [ $this, 'addColumn' ] );
add_action( "manage_{$postType}_posts_custom_column", [ $this, 'renderColumn' ], 10, 2 );
}
/**
* Enqueues the JS/CSS for the page/posts table page.
*
* @since 4.0.0
*
* @return void
*/
public function enqueueScripts() {
$data = aioseo()->helpers->getVueData();
$data['posts'] = [];
$data['terms'] = [];
aioseo()->core->assets->load( $this->scriptSlug, [], $data );
}
/**
* Adds the AIOSEO Details column to the page/post tables in the admin.
*
* @since 4.0.0
*
* @param array $columns The columns we are adding ours onto.
* @return array The modified columns.
*/
public function addColumn( $columns ) {
$canManageSeo = apply_filters( 'aioseo_manage_seo', 'aioseo_manage_seo' );
if (
! current_user_can( $canManageSeo ) &&
(
! current_user_can( 'aioseo_page_general_settings' ) &&
! current_user_can( 'aioseo_page_analysis' )
)
) {
return $columns;
}
// Translators: 1 - The short plugin name ("AIOSEO").
$columns['aioseo-details'] = sprintf( esc_html__( '%1$s Details', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );
return $columns;
}
/**
* Renders the column in the page/post table.
*
* @since 4.0.0
*
* @param string $columnName The column name.
* @param int $postId The current rows, post id.
* @return void
*/
public function renderColumn( $columnName, $postId = 0 ) {
if ( ! current_user_can( 'edit_post', $postId ) && ! current_user_can( 'aioseo_manage_seo' ) ) {
return;
}
if ( 'aioseo-details' !== $columnName ) {
return;
}
// Add this column/post to the localized array.
global $wp_scripts; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
if (
! is_object( $wp_scripts ) || // phpcs:ignore Squiz.NamingConventions.ValidVariableName
! method_exists( $wp_scripts, 'get_data' ) || // phpcs:ignore Squiz.NamingConventions.ValidVariableName
! method_exists( $wp_scripts, 'add_data' ) // phpcs:ignore Squiz.NamingConventions.ValidVariableName
) {
return;
}
$data = null;
if ( is_object( $wp_scripts ) ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName
$data = $wp_scripts->get_data( 'aioseo/js/' . $this->scriptSlug, 'data' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
}
if ( ! is_array( $data ) ) {
$data = json_decode( str_replace( 'var aioseo = ', '', substr( $data, 0, -1 ) ), true );
}
// We have to temporarily modify the query here since the query incorrectly identifies
// the current page as a category page when posts are filtered by a specific category.
// phpcs:disable Squiz.NamingConventions.ValidVariableName
global $wp_query;
$originalQuery = clone $wp_query;
$wp_query->is_category = false;
$wp_query->is_tag = false;
$wp_query->is_tax = false;
// phpcs:enable Squiz.NamingConventions.ValidVariableName
$posts = ! empty( $data['posts'] ) ? $data['posts'] : [];
$postData = $this->getPostData( $postId, $columnName );
$addonsColumnData = array_filter( aioseo()->addons->doAddonFunction( 'admin', 'renderColumnData', [
$columnName,
$postId,
$postData
] ) );
$wp_query = $originalQuery; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
foreach ( $addonsColumnData as $addonColumnData ) {
$postData = array_merge( $postData, $addonColumnData );
}
$posts[] = $postData;
$data['posts'] = $posts;
$wp_scripts->add_data( 'aioseo/js/' . $this->scriptSlug, 'data', '' ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName
wp_localize_script( 'aioseo/js/' . $this->scriptSlug, 'aioseo', $data );
require AIOSEO_DIR . '/app/Common/Views/admin/posts/columns.php';
}
/**
* Gets the post data for the column.
*
* @since 4.5.0
*
* @param int $postId The Post ID.
* @param string $columnName The column name.
* @return array The post data.
*/
protected function getPostData( $postId, $columnName ) {
$nonce = wp_create_nonce( "aioseo_meta_{$columnName}_{$postId}" );
$thePost = Models\Post::getPost( $postId );
$postType = get_post_type( $postId );
$postData = [
'id' => $postId,
'columnName' => $columnName,
'nonce' => $nonce,
'title' => $thePost->title,
'defaultTitle' => aioseo()->meta->title->getPostTypeTitle( $postType ),
'showTitle' => apply_filters( 'aioseo_details_column_post_show_title', true, $postId ),
'description' => $thePost->description,
'defaultDescription' => aioseo()->meta->description->getPostTypeDescription( $postType ),
'showDescription' => apply_filters( 'aioseo_details_column_post_show_description', true, $postId ),
'value' => ! empty( $thePost->seo_score ) ? (int) $thePost->seo_score : 0,
'showMedia' => false,
'isSpecialPage' => aioseo()->helpers->isSpecialPage( $postId ),
'postType' => $postType,
'isPostVisible' => aioseo()->helpers->isPostPubliclyViewable( $postId )
];
return $postData;
}
/**
* Checks whether the AIOSEO Details column should be registered.
*
* @since 4.0.0
*
* @return bool Whether the column should be registered.
*/
public function shouldRegisterColumn( $screen, $postType ) {
// Only allow users with the correct permissions to see the column.
if ( ! current_user_can( 'aioseo_page_general_settings' ) ) {
return false;
}
if ( 'type' === $postType ) {
$postType = '_aioseo_type';
}
if ( 'edit' === $screen || 'upload' === $screen ) {
if (
aioseo()->options->advanced->postTypes->all &&
in_array( $postType, aioseo()->helpers->getPublicPostTypes( true ), true )
) {
return true;
}
$postTypes = aioseo()->options->advanced->postTypes->included;
if ( in_array( $postType, $postTypes, true ) ) {
return true;
}
}
return false;
}
} Standalone/Notifications.php 0000666 00000001357 15113050717 0012170 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Handles the notifications standalone.
*
* @since 4.2.0
*/
class Notifications {
/**
* Class constructor.
*
* @since 4.2.0
*/
public function __construct() {
if ( ! is_admin() ) {
return;
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScript' ] );
}
/**
* Enqueues the script.
*
* @since 4.2.0
*
* @return void
*/
public function enqueueScript() {
aioseo()->core->assets->load( 'src/vue/standalone/notifications/main.js', [], [
'newNotifications' => count( Models\Notification::getNewNotifications() )
], 'aioseoNotifications' );
}
} Standalone/AdminBarNoindexWarning.php 0000666 00000003166 15113050717 0013707 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Handles the admin bar noindex warning.
*
* @since 4.6.7
*/
class AdminBarNoindexWarning {
/**
* Class constructor.
*
* @since 4.6.7
*/
public function __construct() {
add_action( 'init', [ $this, 'init' ] );
}
/**
* Initializes the standalone.
*
* @since 4.6.7
*
* @return void
*/
public function init() {
if ( wp_doing_ajax() || wp_doing_cron() ) {
return;
}
$isSitePublic = get_option( 'blog_public' );
if ( $isSitePublic ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScript' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueueScript' ] );
add_action( 'admin_bar_menu', [ $this, 'addAdminBarElement' ], 99999 );
}
/**
* Enqueues the script.
*
* @since 4.6.7
*
* @return void
*/
public function enqueueScript() {
aioseo()->core->assets->load( 'src/vue/standalone/admin-bar-noindex-warning/main.js', [], [
'optionsReadingUrl' => admin_url( 'options-reading.php' ),
], 'aioseoAdminBarNoindexWarning' );
}
/**
* Adds the admin bar element.
*
* @since 4.6.7
*
* @param \WP_Admin_Bar $wpAdminBar The admin bar object.
* @return void
*/
public function addAdminBarElement( $wpAdminBar ) {
$wpAdminBar->add_node(
[
'id' => 'aioseo-admin-bar-noindex-warning',
'title' => __( 'Search Engines Blocked!', 'all-in-one-seo-pack' ),
'href' => admin_url( 'options-reading.php' )
]
);
}
} Standalone/PageBuilders/ThriveArchitect.php 0000666 00000031364 15113050717 0015016 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Integrate our SEO Panel with Thrive Architect Page Builder.
*
* @since 4.6.6
*/
class ThriveArchitect extends Base {
/**
* The plugin files.
*
* @since 4.6.6
*
* @var array
*/
public $plugins = [
'thrive-visual-editor/thrive-visual-editor.php'
];
/**
* The integration slug.
*
* @since 4.6.6
*
* @var string
*/
public $integrationSlug = 'thrive-architect';
/**
* Init the integration.
*
* @since 4.6.6
*
* @return void
*/
public function init() {
add_filter( 'tcb_allowed_ajax_options', [ $this, 'makeSettingsAllowed' ] );
if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( get_post_type( $this->getPostId() ) ) ) {
return;
}
add_action( 'tcb_main_frame_enqueue', [ $this, 'enqueue' ] );
add_filter( 'tve_main_js_dependencies', [ $this, 'mainJsDependencies' ] );
add_action( 'tcb_right_sidebar_content_settings', [ $this, 'addSettingsTab' ] );
add_action( 'tcb_sidebar_extra_links', [ $this, 'addSidebarButton' ] );
add_filter( 'tcb_main_frame_localize', [ $this, 'localizeData' ] );
}
/**
* Overrides the parent enqueue to add WordPress styles that we need.
*
* @since 4.6.6
*
* @return void
*/
public function enqueue() {
wp_enqueue_style( 'common' );
wp_enqueue_style( 'buttons' );
wp_enqueue_style( 'forms' );
wp_enqueue_style( 'list-tables' );
wp_enqueue_style( 'wp-components' );
print_admin_styles();
parent::enqueue();
}
/**
* Add our javascript to the plugin dependencies.
*
* @since 4.6.6
*
* @param array $dependencies The dependencies.
* @return array The dependencies.
*/
public function mainJsDependencies( $dependencies ) {
$dependencies[] = aioseo()->core->assets->jsHandle( "src/vue/standalone/page-builders/{$this->integrationSlug}/main.js" );
return $dependencies;
}
/**
* Add the extra link to the sidebar.
*
* @since 4.6.6
*
* @return void
*/
public function addSidebarButton() {
$tooltip = sprintf(
// Translators: 1 - The plugin short name ("AIOSEO").
esc_html__( '%1$s Settings', 'all-in-one-seo-pack' ),
AIOSEO_PLUGIN_SHORT_NAME
);
// phpcs:disable Generic.Files.LineLength.MaxExceeded
?>
<a href="javascript:void(0)" class="mouseenter mouseleave sidebar-item tcb-sidebar-icon-aioseo" data-position="left" data-toggle="settings" data-tooltip="<?php echo esc_attr( $tooltip ); ?>">
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0011 19C14.9722 19 19.0021 14.9706 19.0021 10C19.0021 5.02944 14.9722 1 10.0011 1C5.02991 1 1 5.02944 1 10C1 14.9706 5.02991 19 10.0011 19Z" stroke="currentColor"/>
<path d="M9.99664 13.3228C9.99896 13.2104 9.47813 13.1752 9.37307 13.141C8.56228 12.8777 8.04027 12.3293 7.78204 11.5205C7.6493 11.1043 7.68851 10.6765 7.68163 10.2515C7.68162 10.2511 7.68134 10.2507 7.68093 10.2506V10.2506C7.68051 10.2504 7.68023 10.25 7.68023 10.2496C7.68069 9.99579 7.67884 9.74246 7.68115 9.48867C7.683 9.31099 7.74131 9.25453 7.92133 9.25222C8.07128 9.25037 8.22168 9.24482 8.37116 9.25407C8.48454 9.26101 8.86945 9.25407 8.86945 9.25407M9.99664 13.3228C9.98877 13.7037 10.0003 14.1192 10.0008 14.5M9.99664 13.3228C10.0036 13.7152 9.99664 14.1076 10.0008 14.5M9.99664 13.3228C9.99863 13.2265 10.5453 13.1946 10.6332 13.1715C11.6884 12.8929 12.42 11.955 12.4311 10.8639C12.4358 10.4137 12.433 9.96389 12.432 9.51366C12.432 9.30312 12.3788 9.25268 12.1641 9.25176C12.0197 9.25083 11.8749 9.24435 11.7314 9.25407C11.6213 9.26148 11.2793 9.25176 11.2793 9.25176M10.0008 14.5C10.0008 14.7878 9.72149 14.8117 9.50075 15C9.29018 15.1795 8.77054 15.5587 8.52758 15.6966C8.32442 15.8118 8.12033 15.8428 7.90097 15.7401C7.72373 15.6573 7.53862 15.5916 7.36276 15.5059C7.13877 15.3963 7.04344 15.1936 7.08879 14.947C7.1323 14.7091 7.17765 14.4718 7.22578 14.2348C7.27529 13.9933 7.21745 13.7897 7.02632 13.6291C6.78706 13.4283 6.5677 13.2071 6.37195 12.9637C6.21321 12.7671 6.01098 12.7102 5.76941 12.762C5.5445 12.8101 5.31866 12.8559 5.09282 12.9008C4.84339 12.9503 4.64902 12.8587 4.53425 12.6329C4.42457 12.4173 4.33433 12.1924 4.2589 11.9624C4.1793 11.719 4.24085 11.5191 4.4491 11.3683C4.64532 11.2262 4.84616 11.0911 5.04794 10.9574C5.26961 10.8107 5.34828 10.6071 5.31866 10.348C5.2858 10.0606 5.28673 9.7714 5.31635 9.48405C5.34411 9.21613 5.25711 9.01253 5.02757 8.86585C4.8383 8.74461 4.65318 8.6169 4.46853 8.4878C4.23576 8.32492 4.16541 8.12086 4.26028 7.85525C4.33803 7.6387 4.42318 7.42446 4.51852 7.21531C4.62218 6.98811 4.80822 6.89186 5.05349 6.93258C5.28025 6.97006 5.50609 7.0168 5.731 7.06585C5.99154 7.12322 6.20812 7.06631 6.3775 6.84929C6.56261 6.61238 6.77781 6.40322 7.00503 6.2061C7.18829 6.04693 7.27205 5.8549 7.21143 5.60549C7.16006 5.3931 7.12906 5.17608 7.08509 4.96184C7.01429 4.61803 7.1036 4.42924 7.4257 4.28024C7.6085 4.19603 7.79453 4.11783 7.98335 4.04842C8.22399 3.9605 8.42762 4.02574 8.57385 4.23536C8.71546 4.43849 8.84967 4.64718 8.98341 4.85587C9.12317 5.07382 9.32032 5.15896 9.57392 5.12703C9.86131 5.09094 10.1492 5.09325 10.437 5.12564C10.6763 5.15248 10.8669 5.0715 11.0002 4.86698C11.1261 4.67402 11.251 4.48014 11.3778 4.28765C11.5611 4.01001 11.7467 3.94384 12.054 4.05952C12.2479 4.13217 12.4395 4.21176 12.6264 4.3006C12.8731 4.41813 12.9684 4.6134 12.9198 4.87993C12.8763 5.11777 12.8291 5.35469 12.7828 5.59207C12.737 5.82621 12.7944 6.0261 12.9804 6.18297C13.2234 6.38842 13.4437 6.61561 13.6473 6.86086C13.8005 7.04502 13.993 7.11443 14.2337 7.05474C14.4567 6.99921 14.6839 6.95896 14.9098 6.91407C15.1648 6.86317 15.3661 6.95988 15.4822 7.19217C15.5882 7.40364 15.6761 7.6225 15.7506 7.84693C15.8335 8.09726 15.771 8.29901 15.5558 8.45495C15.3596 8.59654 15.1583 8.73166 14.9565 8.86585C14.7473 9.00466 14.6645 9.19855 14.6913 9.44425C14.7237 9.74363 14.7242 10.0435 14.6946 10.3429C14.6691 10.6038 14.7552 10.8033 14.9783 10.9467C15.1624 11.0652 15.3429 11.1897 15.523 11.3146C15.7816 11.4946 15.8506 11.6973 15.7451 11.9879C15.6766 12.1771 15.5993 12.3636 15.5174 12.5473C15.3818 12.8504 15.1907 12.9401 14.8621 12.8721C14.6423 12.8263 14.4211 12.7888 14.2017 12.7398C13.9939 12.6935 13.8213 12.7574 13.689 12.911C13.4627 13.1738 13.2234 13.4218 12.9638 13.6527C12.8088 13.7906 12.7467 13.9706 12.7958 14.1849C12.8485 14.4148 12.8874 14.6476 12.9337 14.879C12.9957 15.189 12.8999 15.3856 12.6088 15.5235C12.4478 15.5999 12.2789 15.66 12.1178 15.7364C11.911 15.8345 11.7175 15.8058 11.5236 15.7003C11.2265 15.5388 10.741 15.2332 10.5009 15C10.3403 14.8441 10.0031 14.7207 10.0008 14.5ZM11.2793 9.25176C11.2848 8.85382 11.2873 8.01509 11.2804 7.61719C11.2795 7.56905 11.2791 7.5401 11.279 7.52286C11.2788 7.50546 11.2793 7.50858 11.2794 7.52598C11.2798 7.63906 11.2816 8.09163 11.2833 8.28906C11.2796 8.687 11.2714 8.85382 11.2793 9.25176ZM11.2793 9.25176C11.2793 9.25176 10.9086 9.25685 10.7873 9.255C10.2968 9.24806 9.80624 9.24852 9.31569 9.255C9.19999 9.25638 8.86945 9.25407 8.86945 9.25407M8.86945 9.25407C8.87593 8.8677 8.87547 8.34389 8.87408 7.95752C8.87346 7.78806 8.87262 7.62829 8.87143 7.54953C8.8709 7.51441 8.86954 7.51752 8.86963 7.55263C8.86985 7.62907 8.8701 7.7811 8.86945 7.95752C8.86853 8.34435 8.86251 8.8677 8.86945 9.25407Z" stroke="currentColor"/>
</svg>
</a>
<?php
//phpcs:enable Generic.Files.LineLength.MaxExceeded
}
/**
* Adds the settings tab for AIOSEO in the Thrive Architect page builder.
*
* @since 4.6.6
*
* @return void
*/
public function addSettingsTab() {
//phpcs:disable Generic.Files.LineLength.MaxExceeded
?>
<div class="tve-component s-item tcb-aioseo">
<div class="dropdown-header">
<div class="group-description s-name">
<?php echo esc_html( AIOSEO_PLUGIN_SHORT_NAME ); ?>
</div>
</div>
<div class="dropdown-content">
<div class="tcb-aioseo-settings">
<button class="click tcb-settings-modal-open-button s-item inside-button">
<span class="s-name">
<?php
printf(
// Translators: 1 - The plugin short name ("AIOSEO").
esc_html__( '%1$s Settings', 'all-in-one-seo-pack' ),
esc_html( AIOSEO_PLUGIN_SHORT_NAME )
);
?>
</span>
</button>
<div class="mt-10 button-group">
<div id="aioseo-score-btn-settings"></div>
<button type="button" class="p-3 action-btn click" id="settings-action-btn">
<svg class="when-active" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.61433 9.94582C10.145 11.0636 10.2253 12.3535 9.45767 13.2683C9.24696 13.5194 8.89896 13.533 8.66555 13.3372L6.22379 11.2883L4.64347 13.1717C4.62842 13.1896 4.59542 13.1925 4.58036 13.2104L3.42703 13.7098C3.28287 13.7722 3.12128 13.6366 3.15772 13.4838L3.44925 12.2613C3.4643 12.2434 3.47935 12.2254 3.4944 12.2075L5.07472 10.3241L2.63295 8.27524C2.3816 8.06433 2.35256 7.73431 2.56327 7.4832C3.3158 6.58636 4.59718 6.40838 5.7901 6.73691L7.81453 4.7983L7.06045 4.16555C6.80909 3.95464 6.78006 3.62462 6.99077 3.37351L7.7132 2.51255C7.90885 2.27937 8.25395 2.23272 8.50531 2.44363L13.3888 6.54141C13.6222 6.73725 13.6542 7.10028 13.4585 7.33345L12.7361 8.19441C12.5254 8.44552 12.1774 8.45917 11.944 8.26332L11.172 7.61551L9.61433 9.94582Z" fill="#FFFFFF"/>
</svg>
<svg class="when-inactive" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.87874 6.50704C5.88995 6.50899 5.90115 6.51097 5.91236 6.513L7.23686 5.29914L6.55461 4.72665C6.3212 4.5308 6.27436 4.18554 6.48527 3.93418L7.6905 2.49785C7.88635 2.26445 8.24956 2.23267 8.48297 2.42852L13.3665 6.52629C13.6179 6.73721 13.6317 7.08535 13.4358 7.31876L12.2306 8.75509C12.0197 9.00645 11.6895 9.03534 11.4381 8.82442L10.7738 8.26701L9.80841 9.78218C9.81235 9.79286 9.81625 9.80354 9.82011 9.81424L9.23164 9.32046L10.6275 7.16519L11.7766 8.12937L12.7408 6.9803L8.14451 3.12358L7.18033 4.27264L8.3294 5.23682L6.4644 6.99846L5.87874 6.50704ZM4.72914 6.45619C3.84314 6.53709 3.0184 6.91015 2.43354 7.63253C2.31301 7.77616 2.31817 8.02525 2.47976 8.16084L5.35242 10.5713L3.54458 12.7258L3.51445 12.7617L3.176 13.4568C3.09138 13.6305 3.28888 13.7962 3.44531 13.6827L4.07103 13.2287L4.10116 13.1928L5.909 11.0383L8.78167 13.4488C8.94326 13.5844 9.18946 13.5462 9.30998 13.4025C9.90734 12.6906 10.1364 11.8178 10.0663 10.9346L9.15211 10.1675C9.42355 10.9846 9.399 11.8779 8.95854 12.6181L3.26707 7.84242C3.90443 7.26739 4.79027 7.09684 5.64573 7.2253L4.72914 6.45619Z" fill="#50565F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4242 11.9993L3.23163 4.28583L3.68158 3.7496L12.8741 11.463L12.4242 11.9993Z" fill="#50565F"/>
</svg>
</button>
</div>
</div>
</div>
</div>
<?php
//phpcs:enable Generic.Files.LineLength.MaxExceeded
}
/**
* Localizes the data by adding the 'is_aioseo_settings_enabled' option to the provided data array.
*
* @since 4.6.6
*
* @param array $data The data array to be localized.
* @return array The localized data array with the 'is_aioseo_settings_enabled' option added.
*/
public function localizeData( $data ) {
// We use get_option here since it is how Thrive Architect saves the settings.
$data['is_aioseo_settings_enabled'] = get_option( 'is_aioseo_settings_enabled', true );
return $data;
}
/**
* Adds 'is_aioseo_settings_enabled' to the list of allowed settings.
*
* @since 4.6.6
*
* @param array $options The array of allowed settings.
* @return array The updated array of allowed settings.
*/
public function makeSettingsAllowed( $options ) {
$options[] = 'is_aioseo_settings_enabled';
return $options;
}
/**
* Returns whether or not the given Post ID was built with Thrive Architect.
*
* @since 4.6.6
*
* @param int $postId The Post ID.
* @return boolean Whether or not the Post was built with Thrive Architect.
*/
public function isBuiltWith( $postId ) {
if ( ! function_exists( 'tcb_post' ) ) {
return false;
}
return tcb_post( $postId )->editor_enabled();
}
/**
* Returns whether should or not limit the modified date.
*
* @since 4.6.6
*
* @param int $postId The Post ID.
* @return boolean Whether or not sholud limit the modified date.
*/
public function limitModifiedDate( $postId ) {
if ( ! class_exists( 'TCB_Editor_Ajax' ) ) {
return false;
}
// This method is supposed to be used in the `wp_ajax_tcb_editor_ajax` action.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), \TCB_Editor_Ajax::NONCE_KEY ) ) {
return false;
}
$editorPostId = ! empty( $_REQUEST['post_id'] ) ? intval( $_REQUEST['post_id'] ) : 0;
if ( $editorPostId !== $postId ) {
return false;
}
return ! empty( $_REQUEST['aioseo_limit_modified_date'] ) && 'true' === $_REQUEST['aioseo_limit_modified_date'];
}
} Standalone/PageBuilders/Elementor.php 0000666 00000013656 15113050717 0013664 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use Elementor\Controls_Manager as ControlsManager;
use Elementor\Core\DocumentTypes\PageBase;
/**
* Integrate our SEO Panel with Elementor Page Builder.
*
* @since 4.1.7
*/
class Elementor extends Base {
/**
* The plugin files.
*
* @since 4.1.7
*
* @var array
*/
public $plugins = [
'elementor/elementor.php',
'elementor-pro/elementor-pro.php',
];
/**
* The integration slug.
*
* @since 4.1.7
*
* @var string
*/
public $integrationSlug = 'elementor';
/**
* Init the integration.
*
* @since 4.1.7
*
* @return void
*/
public function init() {
if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( get_post_type( $this->getPostId() ) ) ) {
return;
}
if ( ! did_action( 'elementor/init' ) ) {
add_action( 'elementor/init', [ $this, 'addPanelTab' ] );
} else {
$this->addPanelTab();
}
add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue' ] );
add_action( 'elementor/documents/register_controls', [ $this, 'registerDocumentControls' ] );
add_action( 'elementor/editor/footer', [ $this, 'addContainers' ] );
// Add the SEO tab to the main Elementor panel.
add_action( 'elementor/editor/footer', [ $this, 'startCapturing' ], 0 );
add_action( 'elementor/editor/footer', [ $this, 'endCapturing' ], 999 );
}
/**
* Start capturing buffer.
*
* @since 4.3.5
*
* @return void
*/
public function startCapturing() {
ob_start();
}
/**
* End capturing buffer and add button.
* This is a hack to add the SEO tab to the main Elementor panel.
* We need to do this because Elementor doesn't provide a filter to add tabs to the main panel.
*
* @since 4.3.5
*
* @return void
*/
public function endCapturing() {
$output = ob_get_clean();
$search = '/(<div class="elementor-component-tab elementor-panel-navigation-tab" data-tab="global">.*<\/div>)/m';
$replace = '${1}<div class="elementor-component-tab elementor-panel-navigation-tab" data-tab="aioseo">SEO</div>';
echo preg_replace( $search, $replace, $output ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Add the AIOSEO Panel Tab on Elementor.
*
* @since 4.1.7
*
* @return void
*/
public function addPanelTab() {
ControlsManager::add_tab( 'aioseo', AIOSEO_PLUGIN_SHORT_NAME );
}
/**
* Register the Elementor Document Controls.
*
* @since 4.1.7
*
* @return void
*/
public function registerDocumentControls( $document ) {
// PageBase is the base class for documents like `post` `page` and etc.
if ( ! $document instanceof PageBase || ! $document::get_property( 'has_elements' ) ) {
return;
}
// This is needed to get the tab to appear, but will be overwritten in the JavaScript.
$document->start_controls_section(
'aioseo_section',
[
'label' => AIOSEO_PLUGIN_SHORT_NAME,
'tab' => 'aioseo',
]
);
$document->end_controls_section();
}
/**
* Returns whether or not the given Post ID was built with Elementor.
*
* @since 4.1.7
*
* @param int $postId The Post ID.
* @return boolean Whether or not the Post was built with Elementor.
*/
public function isBuiltWith( $postId ) {
$document = $this->getElementorDocument( $postId );
if ( ! $document ) {
return false;
}
return $document->is_built_with_elementor();
}
/**
* Returns the Elementor edit url for the given Post ID.
*
* @since 4.3.1
*
* @param int $postId The Post ID.
* @return string The Edit URL.
*/
public function getEditUrl( $postId ) {
$document = $this->getElementorDocument( $postId );
if ( ! $document || ! $document->is_editable_by_current_user() ) {
return '';
}
return esc_url( $document->get_edit_url() );
}
/**
* Add the containers to mount our panel.
*
* @since 4.1.9
*
* @return void
*/
public function addContainers() {
echo '<div id="aioseo-admin"></div>';
}
/**
* Returns the Elementor Document instance for the given Post ID.
*
* @since 4.3.5
*
* @param int $postId The Post ID.
* @return object The Elementor Document instance.
*/
private function getElementorDocument( $postId ) {
if (
! class_exists( '\Elementor\Plugin' ) ||
! is_object( \Elementor\Plugin::instance()->documents ) ||
! method_exists( \Elementor\Plugin::instance()->documents, 'get' )
) {
return false;
}
$elementorDocument = \Elementor\Plugin::instance()->documents->get( $postId );
if ( empty( $elementorDocument ) ) {
return false;
}
return $elementorDocument;
}
/**
* Checks whether or not we should prevent the date from being modified.
* This method is supposed to be used in the `wp_ajax_seedprod_pro_save_lpage` action.
*
* @since 4.5.2
*
* @param int $postId The Post ID.
* @return bool Whether or not we should prevent the date from being modified.
*/
public function limitModifiedDate( $postId ) {
// This method is supposed to be used in the `wp_ajax_elementor_ajax` action.
if ( empty( $_REQUEST['_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_nonce'] ) ), 'elementor_ajax' ) ) {
return false;
}
$editorPostId = ! empty( $_REQUEST['editor_post_id'] ) ? (int) $_REQUEST['editor_post_id'] : false;
if ( $editorPostId !== $postId ) {
return false;
}
return ! empty( $_REQUEST['aioseo_limit_modified_date'] );
}
/**
* Get the post ID.
*
* @since 4.6.9
*
* @return int|null The post ID or null.
*/
public function getPostId() {
// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
if ( aioseo()->helpers->isAjaxCronRestRequest() ) {
foreach ( [ 'editor_post_id', 'initial_document_id' ] as $key ) {
if ( ! empty( $_REQUEST[ $key ] ) ) {
return intval( wp_unslash( $_REQUEST[ $key ] ) );
}
}
}
// phpcs:enable
return parent::getPostId();
}
} Standalone/PageBuilders/Divi.php 0000666 00000012577 15113050717 0012626 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Integrate our SEO Panel with Divi Page Builder.
*
* @since 4.1.7
*/
class Divi extends Base {
/**
* The theme name.
*
* @since 4.1.7
*
* @var array
*/
public $themes = [ 'Divi', 'Extra' ];
/**
* The plugin files.
*
* @since 4.2.0
*
* @var array
*/
public $plugins = [
'divi-builder/divi-builder.php'
];
/**
* The integration slug.
*
* @since 4.1.7
*
* @var string
*/
public $integrationSlug = 'divi';
/**
* Init the integration.
*
* @since 4.1.7
*
* @return void
*/
public function init() {
add_action( 'wp', [ $this, 'maybeRun' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAdmin' ] );
}
/**
* Check if we are in the Page Builder and run the integrations.
*
* @since 4.1.7
*
* @return void
*/
public function maybeRun() {
$postType = get_post_type( $this->getPostId() );
if (
! defined( 'ET_BUILDER_PRODUCT_VERSION' ) ||
! version_compare( '4.9.2', ET_BUILDER_PRODUCT_VERSION, '<=' ) ||
! ( function_exists( 'et_core_is_fb_enabled' ) && et_core_is_fb_enabled() ) ||
! aioseo()->postSettings->canAddPostSettingsMetabox( $postType )
) {
return;
}
add_action( 'wp_footer', [ $this, 'addContainers' ] );
add_action( 'wp_footer', [ $this, 'addIframeWatcher' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue' ] );
add_filter( 'script_loader_tag', [ $this, 'addEtTag' ], 10, 2 );
}
/**
* Enqueue the required scripts for the admin screen.
*
* @since 4.1.7
*
* @return void
*/
public function enqueueAdmin() {
if ( ! aioseo()->helpers->isScreenBase( 'toplevel_page_et_divi_options' ) ) {
return;
}
aioseo()->core->assets->load( 'src/vue/standalone/page-builders/divi-admin/main.js', [], aioseo()->helpers->getVueData() );
aioseo()->main->enqueueTranslations();
}
/**
* Add et attributes to script tags.
*
* @since 4.1.7
*
* @param string $tag The <script> tag for the enqueued script.
* @param string $handle The script's registered handle.
* @return string The tag.
*/
public function addEtTag( $tag, $handle = '' ) {
$scriptHandles = [
'aioseo/js/src/vue/standalone/page-builders/divi/main.js',
'aioseo/js/src/vue/standalone/app/main.js'
];
if ( in_array( $handle, $scriptHandles, true ) ) {
// These tags load in parent window only, not in Divi iframe.
return preg_replace( '/<script/', '<script class="et_fb_ignore_iframe"', (string) $tag );
}
return $tag;
}
/**
* Add the Divi watcher.
*
* @since 4.1.7
*
* @return void
*/
public function addIframeWatcher() {
?>
<script type="text/javascript">
if (typeof jQuery === 'function') {
jQuery(window).on('et_builder_api_ready et_fb_section_content_change', function(event) {
window.parent.postMessage({ eventType : event.type })
})
}
</script>
<?php
}
/**
* Add the containers to mount our panel.
*
* @since 4.1.7
*
* @return void
*/
public function addContainers() {
echo '<div id="aioseo-app-modal" class="et_fb_ignore_iframe"><div class="et_fb_ignore_iframe"></div></div>';
echo '<div id="aioseo-settings" class="et_fb_ignore_iframe"></div>';
echo '<div id="aioseo-admin" class="et_fb_ignore_iframe"></div>';
echo '<div id="aioseo-modal-portal" class="et_fb_ignore_iframe"></div>';
}
/**
* Returns whether or not the given Post ID was built with Divi.
*
* @since 4.1.7
*
* @param int $postId The Post ID.
* @return boolean Whether or not the Post was built with Divi.
*/
public function isBuiltWith( $postId ) {
if ( ! function_exists( 'et_pb_is_pagebuilder_used' ) ) {
return false;
}
return et_pb_is_pagebuilder_used( $postId );
}
/**
* Returns the Divi edit url for the given Post ID.
*
* @since 4.3.1
*
* @param int $postId The Post ID.
* @return string The Edit URL.
*/
public function getEditUrl( $postId ) {
if ( ! function_exists( 'et_fb_get_vb_url' ) ) {
return '';
}
$isDiviLibrary = 'et_pb_layout' === get_post_type( $postId );
$editUrl = $isDiviLibrary ? get_edit_post_link( $postId, 'raw' ) : get_permalink( $postId );
if ( et_pb_is_pagebuilder_used( $postId ) ) {
$editUrl = et_fb_get_vb_url( $editUrl );
} else {
if ( ! et_pb_is_allowed( 'divi_builder_control' ) ) {
// Prevent link when user lacks `Toggle Divi Builder` capability.
return '';
}
$editUrl = add_query_arg(
[ 'et_fb_activation_nonce' => wp_create_nonce( 'et_fb_activation_nonce_' . $postId ) ],
$editUrl
);
}
return $editUrl;
}
/**
* Checks whether or not we should prevent the date from being modified.
*
* @since 4.5.2
*
* @param int $postId The Post ID.
* @return bool Whether or not we should prevent the date from being modified.
*/
public function limitModifiedDate( $postId ) {
// This method is supposed to be used in the `wp_ajax_et_fb_ajax_save` action.
if ( empty( $_REQUEST['et_fb_save_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['et_fb_save_nonce'] ) ), 'et_fb_save_nonce' ) ) {
return false;
}
$editorPostId = ! empty( $_REQUEST['post_id'] ) ? intval( $_REQUEST['post_id'] ) : 0;
if ( $editorPostId !== $postId ) {
return false;
}
return ! empty( $_REQUEST['options']['conditional_tags']['aioseo_limit_modified_date'] );
}
} Standalone/PageBuilders/Base.php 0000666 00000012307 15113050717 0012574 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Base class for each of our page builder integrations.
*
* @since 4.1.7
*/
abstract class Base {
/**
* The plugin files we can integrate with.
*
* @since 4.1.7
*
* @var array
*/
public $plugins = [];
/**
* The themes names we can integrate with.
*
* @since 4.1.7
*
* @var array
*/
public $themes = [];
/**
* The integration slug.
*
* @since 4.1.7
*
* @var string
*/
public $integrationSlug = '';
/**
* Class constructor.
*
* @since 4.1.7
*
* @return void
*/
public function __construct() {
// We need to delay it to give other plugins a chance to register custom post types.
add_action( 'init', [ $this, '_init' ], PHP_INT_MAX );
}
/**
* The internal init function.
*
* @since 4.1.7
*
* @return void
*/
public function _init() {
// Check if we do have an integration slug.
if ( empty( $this->integrationSlug ) ) {
return;
}
// Check if the plugin or theme to integrate with is active.
if ( ! $this->isPluginActive() && ! $this->isThemeActive() ) {
return;
}
// Check if we can proceed with the integration.
if ( apply_filters( 'aioseo_page_builder_integration_disable', false, $this->integrationSlug ) ) {
return;
}
$this->init();
}
/**
* The init function.
*
* @since 4.1.7
*
* @return void
*/
public function init() {}
/**
* Check if the integration is active.
*
* @since 4.4.8
*
* @return bool Whether or not the integration is active.
*/
public function isActive() {
return $this->isPluginActive() || $this->isThemeActive();
}
/**
* Check whether or not the plugin is active.
*
* @since 4.1.7
*
* @return bool Whether or not the plugin is active.
*/
public function isPluginActive() {
include_once ABSPATH . 'wp-admin/includes/plugin.php';
$plugins = apply_filters( 'aioseo_page_builder_integration_plugins', $this->plugins, $this->integrationSlug );
foreach ( $plugins as $basename ) {
if ( is_plugin_active( $basename ) ) {
return true;
}
}
return false;
}
/**
* Check whether or not the theme is active.
*
* @since 4.1.7
*
* @return bool Whether or not the theme is active.
*/
public function isThemeActive() {
$themes = apply_filters( 'aioseo_page_builder_integration_themes', $this->themes, $this->integrationSlug );
$theme = wp_get_theme();
foreach ( $themes as $name ) {
if ( $name === $theme->stylesheet || $name === $theme->template ) {
return true;
}
}
return false;
}
/**
* Enqueue the scripts and styles.
*
* @since 4.1.7
*
* @return void
*/
public function enqueue() {
$integrationSlug = $this->integrationSlug;
aioseo()->core->assets->load( "src/vue/standalone/page-builders/$integrationSlug/main.js", [], aioseo()->helpers->getVueData( 'post', $this->getPostId(), $integrationSlug ) );
aioseo()->core->assets->enqueueCss( 'src/vue/assets/scss/integrations/main.scss' );
aioseo()->admin->addAioseoModalPortal();
aioseo()->main->enqueueTranslations();
}
/**
* Get the post ID.
*
* @since 4.1.7
*
* @return int|null The post ID or null.
*/
public function getPostId() {
// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
foreach ( [ 'id', 'post', 'post_id' ] as $key ) {
if ( ! empty( $_GET[ $key ] ) ) {
return (int) sanitize_text_field( wp_unslash( $_GET[ $key ] ) );
}
}
// phpcs:enable
if ( ! empty( $GLOBALS['post'] ) ) {
return (int) $GLOBALS['post']->ID;
}
return null;
}
/**
* Returns the page builder edit url for the given Post ID.
*
* @since 4.3.1
*
* @param int $postId The Post ID.
* @return string The Edit URL.
*/
public function getEditUrl( $postId ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return '';
}
/**
* Returns whether or not the given Post ID was built with the Page Builder.
*
* @since 4.1.7
*
* @param int $postId The Post ID.
* @return boolean Whether or not the Post was built with the Page Builder.
*/
public function isBuiltWith( $postId ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return false;
}
/**
* Checks whether or not we should prevent the date from being modified.
*
* @since 4.5.2
*
* @param int $postId The Post ID.
* @return bool Whether or not we should prevent the date from being modified.
*/
public function limitModifiedDate( $postId ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return false;
}
/**
* Returns the processed page builder content.
*
* @since 4.5.2
*
* @param int $postId The post id.
* @param string $content The raw content.
* @return string The processed content.
*/
public function processContent( $postId, $content = '' ) {
if ( empty( $content ) ) {
$post = get_post( $postId );
if ( is_a( $post, 'WP_Post' ) ) {
$content = $post->post_content;
}
}
if ( aioseo()->helpers->isAjaxCronRestRequest() ) {
return apply_filters( 'the_content', $content );
}
return $content;
}
} Standalone/PageBuilders/SeedProd.php 0000666 00000007154 15113050717 0013433 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Integrate our SEO Panel with SeedProd Page Builder.
*
* @since 4.1.7
*/
class SeedProd extends Base {
/**
* The plugin files.
*
* @since 4.1.7
*
* @var array
*/
public $plugins = [
'coming-soon/coming-soon.php',
'seedprod-coming-soon-pro-5/seedprod-coming-soon-pro-5.php',
];
/**
* The integration slug.
*
* @since 4.1.7
*
* @var string
*/
public $integrationSlug = 'seedprod';
/**
* Init the integration.
*
* @since 4.1.7
*
* @return void
*/
public function init() {
$postType = get_post_type( $this->getPostId() );
if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( $postType ) ) {
return;
}
// SeedProd de-enqueues and de-register scripts/styles on priority PHP_INT_MAX.
// Thus, we need to enqueue our scripts at the same priority for more compatibility.
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ], PHP_INT_MAX );
add_filter( 'style_loader_tag', [ $this, 'replaceStyleTag' ], 10, 2 );
}
/**
* Enqueue the scripts and styles.
*
* @since 4.1.7
*
* @return void
*/
public function enqueue() {
if ( ! $this->isBuilderScreen() ) {
return;
}
parent::enqueue();
}
/**
* Check whether or not is builder screen.
*
* @since 4.1.7
*
* @return boolean Whether or not is builder screen.
*/
public function isBuilderScreen() {
$currentScreen = aioseo()->helpers->getCurrentScreen();
return $currentScreen && preg_match( '/seedprod.*?_builder$/i', (string) $currentScreen->base );
}
/**
* Replace original tag to prevent being removed by SeedProd.
*
* @param string $tag The <link> tag for the enqueued style.
* @param string $handle The style's registered handle.
* @return string The tag.
*/
public function replaceStyleTag( $tag, $handle = '' ) {
if ( ! $this->isBuilderScreen() ) {
return $tag;
}
$aioseoCommonHandle = 'aioseo-' . $this->integrationSlug . '-common';
if ( $aioseoCommonHandle === $handle ) {
// All the *common.css links are removed from SeedProd.
// https://github.com/awesomemotive/seedprod-plugins/blob/32854442979bfa068aadf9b8a8a929e5f9f353e5/seedprod-pro/resources/views/builder.php#L406
$tag = str_ireplace( 'href=', 'data-href=', $tag );
}
return $tag;
}
/**
* Returns whether or not the given Post ID was built with SeedProd.
*
* @since 4.1.7
*
* @param int $postId The Post ID.
* @return boolean Whether or not the Post was built with SeedProd.
*/
public function isBuiltWith( $postId ) {
$isSeedProd = get_post_meta( $postId, '_seedprod_page', true );
if ( ! empty( $isSeedProd ) ) {
return true;
}
return false;
}
/**
* Checks whether or not we should prevent the date from being modified.
*
* @since 4.5.2
*
* @param int $postId The Post ID.
* @return bool Whether or not we should prevent the date from being modified.
*/
public function limitModifiedDate( $postId ) {
// This method is supposed to be used in the `wp_ajax_seedprod_pro_save_lpage` action.
if ( wp_doing_ajax() && ! check_ajax_referer( 'seedprod_nonce', false, false ) ) {
return false;
}
$landingPageId = ! empty( $_REQUEST['lpage_id'] ) ? (int) $_REQUEST['lpage_id'] : false;
if ( $landingPageId !== $postId ) {
return false;
}
$settings = ! empty( $_REQUEST['settings'] ) ? json_decode( sanitize_text_field( wp_unslash( $_REQUEST['settings'] ) ) ) : false;
if ( empty( $settings ) || empty( $settings->aioseo_limit_modified_date ) ) {
return false;
}
return true;
}
} Standalone/PageBuilders/Avada.php 0000666 00000005617 15113050717 0012744 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Integrate our SEO Panel with Avada Page Builder.
*
* @since 4.5.2
*/
class Avada extends Base {
/**
* The plugin files.
*
* @since 4.5.2
*
* @var array
*/
public $plugins = [
'fusion-builder/fusion-builder.php'
];
/**
* The integration slug.
*
* @since 4.5.2
*
* @var string
*/
public $integrationSlug = 'avada';
/**
* Init the integration.
*
* @since 4.5.2
*
* @return void
*/
public function init() {
add_action( 'fusion_enqueue_live_scripts', [ $this, 'enqueue' ] );
add_action( 'fusion_builder_admin_scripts_hook', [ $this, 'enqueue' ] );
add_action( 'wp_footer', [ $this, 'addSidebarWrapper' ] );
}
/**
* Check if we are in the front-end builder.
*
* @since 4.5.2
*
* @return boolean Whether or not we are in the front-end builder.
*/
public function isBuilder() {
return function_exists( 'fusion_is_builder_frame' ) && fusion_is_builder_frame();
}
/**
* Check if we are in the front-end preview.
*
* @since 4.5.2
*
* @return boolean Whether or not we are in the front-end preview.
*/
public function isPreview() {
return function_exists( 'fusion_is_preview_frame' ) && fusion_is_preview_frame();
}
/**
* Adds the sidebar wrapper in footer when is in page builder.
*
* @since 4.5.2
*
* @return void
*/
public function addSidebarWrapper() {
if ( ! $this->isBuilder() ) {
return;
}
echo '<div id="fusion-builder-aioseo-sidebar"></div>';
}
/**
* Enqueue the scripts and styles.
*
* @since 4.5.2
*
* @return void
*/
public function enqueue() {
if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( get_post_type( $this->getPostId() ) ) ) {
return;
}
parent::enqueue();
}
/**
* Returns whether or not the given Post ID was built with WPBakery.
*
* @since 4.5.2
*
* @param int $postId The Post ID.
* @return boolean Whether or not the Post was built with WPBakery.
*/
public function isBuiltWith( $postId ) {
return 'active' === get_post_meta( $postId, 'fusion_builder_status', true );
}
/**
* Returns whether should or not limit the modified date.
*
* @since 4.5.2
*
* @param int $postId The Post ID.
* @return boolean Whether or not sholud limit the modified date.
*/
public function limitModifiedDate( $postId ) {
// This method is supposed to be used in the `wp_ajax_fusion_app_save_post_content` action.
if ( ! isset( $_POST['fusion_load_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['fusion_load_nonce'] ) ), 'fusion_load_nonce' ) ) {
return false;
}
$editorPostId = ! empty( $_REQUEST['post_id'] ) ? intval( $_REQUEST['post_id'] ) : 0;
if ( $editorPostId !== $postId ) {
return false;
}
return ! empty( $_REQUEST['query']['aioseo_limit_modified_date'] );
}
} Standalone/PageBuilders/SiteOrigin.php 0000666 00000005157 15113050717 0014003 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Integrate our SEO Panel with SiteOrigin Page Builder.
*
* @since 4.6.6
*/
class SiteOrigin extends Base {
/**
* The plugin files.
*
* @since 4.6.6
*
* @var array
*/
public $plugins = [
'siteorigin-panels/siteorigin-panels.php'
];
/**
* The integration slug.
*
* @since 4.6.6
*
* @var string
*/
public $integrationSlug = 'siteorigin';
/**
* Init the integration.
*
* @since 4.6.6
*
* @return void
*/
public function init() {
$postType = get_post_type( $this->getPostId() );
if ( empty( $postType ) ) {
$postType = ! empty( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : 'post'; // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended, Generic.Files.LineLength.MaxExceeded
}
if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( $postType ) ) {
return;
}
add_action( 'siteorigin_panel_enqueue_admin_scripts', [ $this, 'enqueue' ] );
}
/**
* Returns whether or not the given Post ID was built with SiteOrigin.
*
* @since 4.6.6
*
* @param int $postId The Post ID.
* @return bool Whether or not the Post was built with SiteOrigin.
*/
public function isBuiltWith( $postId ) {
$postObj = get_post( $postId );
if (
! empty( $postObj ) &&
(
preg_match( '/siteorigin_widget/', (string) $postObj->post_content ) ||
preg_match( '/so-panel widget/', (string) $postObj->post_content )
)
) {
return true;
}
return false;
}
/**
* Returns the processed page builder content.
*
* @since 4.6.6
*
* @param int $postId The post id.
* @param string $content The raw content.
* @return string The processed content.
*/
public function processContent( $postId, $content = '' ) {
// When performing a save_post action, we must execute the siteorigin_widget shortcodes if there are image widgets.
// This ensures that the getFirstImageInContent method can locate the images, as SiteOrigin uses shortcodes for images.
// We cache the first image in the content during post saving.
if (
doing_action( 'save_post' ) &&
aioseo()->options->searchAppearance->advanced->runShortcodes &&
(
stripos( $content, 'SiteOrigin_Widget_Image_Widget' ) !== false ||
stripos( $content, 'WP_Widget_Media_Image' ) !== false
)
) {
$content = aioseo()->helpers->doAllowedShortcodes( $content, $postId, [ 'siteorigin_widget' ] );
}
return parent::processContent( $postId, $content );
}
} Standalone/PageBuilders/WPBakery.php 0000666 00000006772 15113050717 0013417 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Standalone\PageBuilders;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Integrate our SEO Panel with WPBakery Page Builder.
*
* @since 4.5.2
*/
class WPBakery extends Base {
/**
* The plugin files.
*
* @since 4.5.2
*
* @var array
*/
public $plugins = [
'js_composer/js_composer.php',
'js_composer_salient/js_composer.php'
];
/**
* The integration slug.
*
* @since 4.5.2
*
* @var string
*/
public $integrationSlug = 'wpbakery';
/**
* Init the integration.
*
* @since 4.5.2
*
* @return void
*/
public function init() {
// Disable SEO meta tags from WP Bakery.
if ( defined( 'WPB_VC_VERSION' ) && version_compare( WPB_VC_VERSION, '7.4', '>=' ) ) {
add_filter( 'get_post_metadata', [ $this, 'maybeDisableWpBakeryMetaTags' ], 10, 3 );
}
if ( ! aioseo()->postSettings->canAddPostSettingsMetabox( get_post_type( $this->getPostId() ) ) ) {
return;
}
add_action( 'vc_frontend_editor_enqueue_js_css', [ $this, 'enqueue' ] );
add_action( 'vc_backend_editor_enqueue_js_css', [ $this, 'enqueue' ] );
add_filter( 'vc_nav_front_controls', [ $this, 'addNavbarCotnrols' ] );
add_filter( 'vc_nav_controls', [ $this, 'addNavbarCotnrols' ] );
}
/**
* Maybe disable WP Bakery meta tags.
*
* @since 4.7.1
*
* @param mixed $value The value of the meta.
* @param int $objectId The object ID.
* @param string $metaKey The meta key.
* @return mixed The value of the meta.
*/
public function maybeDisableWpBakeryMetaTags( $value, $objectId, $metaKey ) {
if ( is_singular() && '_wpb_post_custom_seo_settings' === $metaKey ) {
return null;
}
return $value;
}
public function addNavbarCotnrols( $controlList ) {
$controlList[] = [
'aioseo',
'<li class="vc_show-mobile"><div id="aioseo-wpbakery" style="height: 100%;"></div></li>'
];
return $controlList;
}
/**
* Returns whether or not the given Post ID was built with WPBakery.
*
* @since 4.5.2
*
* @param int $postId The Post ID.
* @return boolean Whether or not the Post was built with WPBakery.
*/
public function isBuiltWith( $postId ) {
$postObj = get_post( $postId );
if ( ! empty( $postObj ) && preg_match( '/vc_row/', (string) $postObj->post_content ) ) {
return true;
}
return false;
}
/**
* Returns whether should or not limit the modified date.
*
* @since 4.5.2
*
* @param int $postId The Post ID.
* @return boolean Whether or not sholud limit the modified date.
*/
public function limitModifiedDate( $postId ) {
// This method is supposed to be used in the `saveAjaxFe` action.
if ( empty( $_REQUEST['_vcnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_vcnonce'] ) ), 'vc-nonce-vc-admin-nonce' ) ) {
return false;
}
$editorPostId = ! empty( $_REQUEST['post_id'] ) ? intval( $_REQUEST['post_id'] ) : 0;
if ( $editorPostId !== $postId ) {
return false;
}
return ! empty( $_REQUEST['aioseo_limit_modified_date'] ) && (bool) $_REQUEST['aioseo_limit_modified_date'];
}
/**
* Returns the processed page builder content.
*
* @since 4.5.2
*
* @param int $postId The post id.
* @param string $content The raw content.
* @return string The processed content.
*/
public function processContent( $postId, $content = '' ) {
if ( method_exists( '\WPBMap', 'addAllMappedShortcodes' ) ) {
\WPBMap::addAllMappedShortcodes();
}
return parent::processContent( $postId, $content );
}
} Options/DynamicOptions.php 0000666 00000026113 15113050717 0011657 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Options;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Traits;
/**
* Handles the dynamic options.
*
* @since 4.1.4
*/
class DynamicOptions {
use Traits\Options;
/**
* The default options.
*
* @since 4.1.4
*
* @var array
*/
protected $defaults = [
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
'sitemap' => [
'priority' => [
'postTypes' => [],
'taxonomies' => []
]
],
'social' => [
'facebook' => [
'general' => [
'postTypes' => []
]
]
],
'searchAppearance' => [
'postTypes' => [],
'taxonomies' => [],
'archives' => []
]
// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
];
/**
* Class constructor.
*
* @since 4.1.4
*
* @param string $optionsName The options name.
*/
public function __construct( $optionsName = 'aioseo_options_dynamic' ) {
$this->optionsName = $optionsName;
// Load defaults in case this is a complete fresh install.
$this->init();
add_action( 'shutdown', [ $this, 'save' ] );
}
/**
* Initializes the options.
*
* @since 4.1.4
*
* @return void
*/
protected function init() {
$this->addDynamicDefaults();
$this->setDbOptions();
}
/**
* Sets the DB options.
*
* @since 4.1.4
*
* @return void
*/
protected function setDbOptions() {
$dbOptions = $this->getDbOptions( $this->optionsName );
// Refactor options.
$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->defaultsMerged );
$dbOptions = array_replace_recursive(
$this->defaultsMerged,
$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
);
aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );
// Get the localized options.
$dbOptionsLocalized = get_option( $this->optionsName . '_localized' );
if ( empty( $dbOptionsLocalized ) ) {
$dbOptionsLocalized = [];
}
$this->localized = $dbOptionsLocalized;
}
/**
* Sanitizes, then saves the options to the database.
*
* @since 4.1.4
*
* @param array $options An array of options to sanitize, then save.
* @return void
*/
public function sanitizeAndSave( $options ) {
if ( ! is_array( $options ) ) {
return;
}
$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
aioseo()->dynamicBackup->maybeBackup( $cachedOptions );
// First, recursively replace the new options into the cached state.
// It's important we use the helper method since we want to replace populated arrays with empty ones if needed (when a setting was cleared out).
$dbOptions = aioseo()->helpers->arrayReplaceRecursive(
$cachedOptions,
$this->addValueToValuesArray( $cachedOptions, $options, [], true )
);
// Now, we must also intersect both arrays to delete any individual keys that were unset.
// We must do this because, while arrayReplaceRecursive will update the values for keys or empty them out,
// it will keys that aren't present in the replacement array unaffected in the target array.
$dbOptions = aioseo()->helpers->arrayIntersectRecursive(
$dbOptions,
$this->addValueToValuesArray( $cachedOptions, $options, [], true ),
'value'
);
// Update the cache state.
aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );
// Update localized options.
update_option( $this->optionsName . '_localized', $this->localized );
// Finally, save the new values to the DB.
$this->save( true );
}
/**
* Adds some defaults that are dynamically generated.
*
* @since 4.1.4
*
* @return void
*/
public function addDynamicDefaults() {
$this->addDynamicPostTypeDefaults();
$this->addDynamicTaxonomyDefaults();
$this->addDynamicArchiveDefaults();
}
/**
* Adds the dynamic defaults for the public post types.
*
* @since 4.1.4
*
* @return void
*/
protected function addDynamicPostTypeDefaults() {
$postTypes = aioseo()->helpers->getPublicPostTypes( false, false, false, [ 'include' => [ 'buddypress' ] ] );
foreach ( $postTypes as $postType ) {
if ( 'type' === $postType['name'] ) {
$postType['name'] = '_aioseo_type';
}
$defaultTitle = '#post_title #separator_sa #site_title';
if ( ! empty( $postType['defaultTitle'] ) ) {
$defaultTitle = $postType['defaultTitle'];
}
$defaultDescription = ! empty( $postType['supports']['excerpt'] ) ? '#post_excerpt' : '#post_content';
if ( ! empty( $postType['defaultDescription'] ) ) {
$defaultDescription = $postType['defaultDescription'];
}
$defaultSchemaType = 'WebPage';
$defaultWebPageType = 'WebPage';
$defaultArticleType = 'BlogPosting';
switch ( $postType['name'] ) {
case 'post':
$defaultSchemaType = 'Article';
break;
case 'attachment':
$defaultDescription = '#attachment_caption';
$defaultSchemaType = 'ItemPage';
$defaultWebPageType = 'ItemPage';
break;
case 'product':
$defaultSchemaType = 'WebPage';
$defaultWebPageType = 'ItemPage';
break;
case 'news':
$defaultArticleType = 'NewsArticle';
break;
case 'web-story':
$defaultWebPageType = 'WebPage';
$defaultSchemaType = 'WebPage';
break;
default:
break;
}
$defaultOptions = array_replace_recursive(
$this->getDefaultSearchAppearanceOptions(),
[
'title' => [
'type' => 'string',
'localized' => true,
'default' => $defaultTitle
],
'metaDescription' => [
'type' => 'string',
'localized' => true,
'default' => $defaultDescription
],
'schemaType' => [
'type' => 'string',
'default' => $defaultSchemaType
],
'webPageType' => [
'type' => 'string',
'default' => $defaultWebPageType
],
'articleType' => [
'type' => 'string',
'default' => $defaultArticleType
],
'customFields' => [ 'type' => 'html' ],
'advanced' => [
'bulkEditing' => [
'type' => 'string',
'default' => 'enabled'
]
]
]
);
if ( 'attachment' === $postType['name'] ) {
$defaultOptions['redirectAttachmentUrls'] = [
'type' => 'string',
'default' => 'attachment'
];
}
$this->defaults['searchAppearance']['postTypes'][ $postType['name'] ] = $defaultOptions;
$this->setDynamicSocialOptions( 'postTypes', $postType['name'] );
$this->setDynamicSitemapOptions( 'postTypes', $postType['name'] );
}
}
/**
* Adds the dynamic defaults for the public taxonomies.
*
* @since 4.1.4
*
* @return void
*/
protected function addDynamicTaxonomyDefaults() {
$taxonomies = aioseo()->helpers->getPublicTaxonomies();
foreach ( $taxonomies as $taxonomy ) {
if ( 'type' === $taxonomy['name'] ) {
$taxonomy['name'] = '_aioseo_type';
}
$defaultOptions = array_replace_recursive(
$this->getDefaultSearchAppearanceOptions(),
[
'title' => [
'type' => 'string',
'localized' => true,
'default' => '#taxonomy_title #separator_sa #site_title'
],
'metaDescription' => [
'type' => 'string',
'localized' => true,
'default' => '#taxonomy_description'
],
]
);
$this->setDynamicSitemapOptions( 'taxonomies', $taxonomy['name'] );
$this->defaults['searchAppearance']['taxonomies'][ $taxonomy['name'] ] = $defaultOptions;
}
}
/**
* Adds the dynamic defaults for the archive pages.
*
* @since 4.1.4
*
* @return void
*/
protected function addDynamicArchiveDefaults() {
$postTypes = aioseo()->helpers->getPublicPostTypes( false, true, false, [ 'include' => [ 'buddypress' ] ] );
foreach ( $postTypes as $postType ) {
if ( 'type' === $postType['name'] ) {
$postType['name'] = '_aioseo_type';
}
$defaultOptions = array_replace_recursive(
$this->getDefaultSearchAppearanceOptions(),
[
'title' => [
'type' => 'string',
'localized' => true,
'default' => '#archive_title #separator_sa #site_title'
],
'metaDescription' => [
'type' => 'string',
'localized' => true,
'default' => ''
],
'customFields' => [ 'type' => 'html' ],
'advanced' => [
'keywords' => [
'type' => 'string',
'localized' => true
]
]
]
);
$this->defaults['searchAppearance']['archives'][ $postType['name'] ] = $defaultOptions;
}
}
/**
* Returns the search appearance options for dynamic objects.
*
* @since 4.1.4
*
* @return array The default options.
*/
protected function getDefaultSearchAppearanceOptions() {
return [ // phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
'show' => [ 'type' => 'boolean', 'default' => true ],
'advanced' => [
'robotsMeta' => [
'default' => [ 'type' => 'boolean', 'default' => true ],
'noindex' => [ 'type' => 'boolean', 'default' => false ],
'nofollow' => [ 'type' => 'boolean', 'default' => false ],
'noarchive' => [ 'type' => 'boolean', 'default' => false ],
'noimageindex' => [ 'type' => 'boolean', 'default' => false ],
'notranslate' => [ 'type' => 'boolean', 'default' => false ],
'nosnippet' => [ 'type' => 'boolean', 'default' => false ],
'noodp' => [ 'type' => 'boolean', 'default' => false ],
'maxSnippet' => [ 'type' => 'number', 'default' => -1 ],
'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
],
'showDateInGooglePreview' => [ 'type' => 'boolean', 'default' => true ],
'showPostThumbnailInSearch' => [ 'type' => 'boolean', 'default' => true ],
'showMetaBox' => [ 'type' => 'boolean', 'default' => true ]
]
]; // phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
}
/**
* Sets the dynamic social settings for a given post type or taxonomy.
*
* @since 4.1.4
*
* @param string $objectType Whether the object belongs to the dynamic "postTypes" or "taxonomies".
* @param string $objectName The object name.
* @return void
*/
protected function setDynamicSocialOptions( $objectType, $objectName ) {
$defaultOptions = [
'objectType' => [
'type' => 'string',
'default' => 'article'
]
];
$this->defaults['social']['facebook']['general'][ $objectType ][ $objectName ] = $defaultOptions;
}
/**
* Sets the dynamic sitemap settings for a given post type or taxonomy.
*
* @since 4.1.4
*
* @param string $objectType Whether the object belongs to the dynamic "postTypes" or "taxonomies".
* @param string $objectName The object name.
* @return void
*/
protected function setDynamicSitemapOptions( $objectType, $objectName ) {
$this->defaults['sitemap']['priority'][ $objectType ][ $objectName ] = [
'priority' => [
'type' => 'string',
'default' => '{"label":"default","value":"default"}'
],
'frequency' => [
'type' => 'string',
'default' => '{"label":"default","value":"default"}'
]
];
}
} Options/NetworkOptions.php 0000666 00000003622 15113050717 0011724 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Options;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Traits;
use AIOSEO\Plugin\Common\Utils;
/**
* Class that holds all network options for AIOSEO.
*
* @since 4.2.5
*/
class NetworkOptions {
use Traits\Options;
use Traits\NetworkOptions;
/**
* Holds the helpers class.
*
* @since 4.2.5
*
* @var Utils\Helpers
*/
protected $helpers;
/**
* All the default options.
*
* @since 4.2.5
*
* @var array
*/
protected $defaults = [
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
'searchAppearance' => [
'advanced' => [
'unwantedBots' => [
'all' => [ 'type' => 'boolean', 'default' => false ],
'settings' => [
'googleAdsBot' => [ 'type' => 'boolean', 'default' => false ],
'openAiGptBot' => [ 'type' => 'boolean', 'default' => false ],
'commonCrawlCcBot' => [ 'type' => 'boolean', 'default' => false ],
'googleGeminiVertexAiBots' => [ 'type' => 'boolean', 'default' => false ]
]
],
'searchCleanup' => [
'settings' => [
'preventCrawling' => [ 'type' => 'boolean', 'default' => false ]
]
]
]
],
'tools' => [
'robots' => [
'enable' => [ 'type' => 'boolean', 'default' => false ],
'rules' => [ 'type' => 'array', 'default' => [] ],
'robotsDetected' => [ 'type' => 'boolean', 'default' => true ],
]
]
// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
];
/**
* The Construct method.
*
* @since 4.2.5
*
* @param string $optionsName The options name.
*/
public function __construct( $optionsName = 'aioseo_options_network' ) {
$this->helpers = new Utils\Helpers();
$this->optionsName = $optionsName;
$this->init();
add_action( 'shutdown', [ $this, 'save' ] );
}
} Options/InternalNetworkOptions.php 0000666 00000001622 15113050717 0013417 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Options;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Traits;
use AIOSEO\Plugin\Common\Utils;
/**
* Class that holds all internal network options for AIOSEO.
*
* @since 4.2.5
*/
class InternalNetworkOptions {
use Traits\Options;
use Traits\NetworkOptions;
/**
* Holds the helpers class.
*
* @since 4.2.5
*
* @var Utils\Helpers
*/
protected $helpers;
/**
* All the default options.
*
* @since 4.2.5
*
* @var array
*/
protected $defaults = [];
/**
* The Construct method.
*
* @since 4.2.5
*
* @param string $optionsName The options name.
*/
public function __construct( $optionsName = 'aioseo_options_network_internal' ) {
$this->helpers = new Utils\Helpers();
$this->optionsName = $optionsName;
$this->init();
add_action( 'shutdown', [ $this, 'save' ] );
}
} Options/Cache.php 0000666 00000003103 15113050717 0007714 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Options;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class that holds all the cache for the AIOSEO options.
*
* @since 4.1.4
*/
class Cache {
/**
* The DB options cache.
*
* @since 4.1.4
*
* @var array
*/
private static $db = [];
/**
* The options cache.
*
* @since 4.1.4
*
* @var array
*/
private static $options = [];
/**
* Sets the cache for the DB option.
*
* @since 4.1.4
*
* @param string $name The cache name.
* @param array $value The value.
* @return void
*/
public function setDb( $name, $value ) {
self::$db[ $name ] = $value;
}
/**
* Gets the cache for the DB option.
*
* @since 4.1.4
*
* @param string $name The cache name.
* @return array The data from the cache.
*/
public function getDb( $name ) {
return ! empty( self::$db[ $name ] ) ? self::$db[ $name ] : [];
}
/**
* Sets the cache for the options.
*
* @since 4.1.4
*
* @param string $name The cache name.
* @param array $value The value.
* @return void
*/
public function setOptions( $name, $value ) {
self::$options[ $name ] = $value;
}
/**
* Gets the cache for the options.
*
* @since 4.1.4
*
* @param string $name The cache name.
* @return array The data from the cache.
*/
public function getOptions( $name ) {
return ! empty( self::$options[ $name ] ) ? self::$options[ $name ] : [];
}
/**
* Resets the DB cache.
*
* @since 4.1.4
*
* @return void
*/
public function resetDb() {
self::$db = [];
}
} Options/Options.php 0000666 00000106145 15113050717 0010356 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Options;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
use AIOSEO\Plugin\Common\Traits;
/**
* Class that holds all options for AIOSEO.
*
* @since 4.0.0
*/
class Options {
use Traits\Options;
/**
* All the default options.
*
* @since 4.0.0
*
* @var array
*/
protected $defaults = [
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
'internal' => [],
'webmasterTools' => [
'google' => [ 'type' => 'string' ],
'bing' => [ 'type' => 'string' ],
'yandex' => [ 'type' => 'string' ],
'baidu' => [ 'type' => 'string' ],
'pinterest' => [ 'type' => 'string' ],
'microsoftClarityProjectId' => [ 'type' => 'string' ],
'norton' => [ 'type' => 'string' ],
'miscellaneousVerification' => [ 'type' => 'html' ]
],
'breadcrumbs' => [
'separator' => [ 'type' => 'string', 'default' => '»' ],
'homepageLink' => [ 'type' => 'boolean', 'default' => true ],
'homepageLabel' => [ 'type' => 'string', 'default' => 'Home' ],
'breadcrumbPrefix' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
'archiveFormat' => [ 'type' => 'string', 'default' => 'Archives for #breadcrumb_archive_post_type_name', 'localized' => true ],
'searchResultFormat' => [ 'type' => 'string', 'default' => 'Search Results for \'#breadcrumb_search_string\'', 'localized' => true ],
'errorFormat404' => [ 'type' => 'string', 'default' => '404 - Page Not Found', 'localized' => true ],
'showCurrentItem' => [ 'type' => 'boolean', 'default' => true ],
'linkCurrentItem' => [ 'type' => 'boolean', 'default' => false ],
'categoryFullHierarchy' => [ 'type' => 'boolean', 'default' => false ],
'showBlogHome' => [ 'type' => 'boolean', 'default' => false ]
],
'rssContent' => [
'before' => [ 'type' => 'html' ],
'after' => [
'type' => 'html',
'default' => <<<TEMPLATE
<p>The post #post_link first appeared on #site_link.</p>
TEMPLATE
]
],
'advanced' => [
'truSeo' => [ 'type' => 'boolean', 'default' => true ],
'headlineAnalyzer' => [ 'type' => 'boolean', 'default' => true ],
'seoAnalysis' => [ 'type' => 'boolean', 'default' => true ],
'dashboardWidgets' => [ 'type' => 'array', 'default' => [ 'seoSetup', 'seoOverview', 'seoNews' ] ],
'announcements' => [ 'type' => 'boolean', 'default' => true ],
'postTypes' => [
'all' => [ 'type' => 'boolean', 'default' => true ],
'included' => [ 'type' => 'array', 'default' => [ 'post', 'page', 'product' ] ],
],
'taxonomies' => [
'all' => [ 'type' => 'boolean', 'default' => true ],
'included' => [ 'type' => 'array', 'default' => [ 'category', 'post_tag', 'product_cat', 'product_tag' ] ],
],
'uninstall' => [ 'type' => 'boolean', 'default' => false ],
'emailSummary' => [
'enable' => [ 'type' => 'boolean', 'default' => false ],
'recipients' => [ 'type' => 'array', 'default' => [] ]
]
],
'sitemap' => [
'general' => [
'enable' => [ 'type' => 'boolean', 'default' => true ],
'filename' => [ 'type' => 'string', 'default' => 'sitemap' ],
'indexes' => [ 'type' => 'boolean', 'default' => true ],
'linksPerIndex' => [ 'type' => 'number', 'default' => 1000 ],
// @TODO: [V4+] Convert this to the dynamic options like in search appearance so we can have backups when plugins are deactivated.
'postTypes' => [
'all' => [ 'type' => 'boolean', 'default' => true ],
'included' => [ 'type' => 'array', 'default' => [ 'post', 'page', 'attachment', 'product' ] ],
],
// @TODO: [V4+] Convert this to the dynamic options like in search appearance so we can have backups when plugins are deactivated.
'taxonomies' => [
'all' => [ 'type' => 'boolean', 'default' => true ],
'included' => [ 'type' => 'array', 'default' => [ 'category', 'post_tag', 'product_cat', 'product_tag' ] ],
],
'author' => [ 'type' => 'boolean', 'default' => false ],
'date' => [ 'type' => 'boolean', 'default' => false ],
'additionalPages' => [
'enable' => [ 'type' => 'boolean', 'default' => false ],
'pages' => [ 'type' => 'array', 'default' => [] ]
],
'advancedSettings' => [
'enable' => [ 'type' => 'boolean', 'default' => false ],
'excludeImages' => [ 'type' => 'boolean', 'default' => false ],
'excludePosts' => [ 'type' => 'array', 'default' => [] ],
'excludeTerms' => [ 'type' => 'array', 'default' => [] ],
'priority' => [
'homePage' => [
'priority' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
],
'postTypes' => [
'grouped' => [ 'type' => 'boolean', 'default' => true ],
'priority' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
],
'taxonomies' => [
'grouped' => [ 'type' => 'boolean', 'default' => true ],
'priority' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
],
'archive' => [
'priority' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
],
'author' => [
'priority' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ],
'frequency' => [ 'type' => 'string', 'default' => '{"label":"default","value":"default"}' ]
]
]
]
],
'rss' => [
'enable' => [ 'type' => 'boolean', 'default' => true ],
'linksPerIndex' => [ 'type' => 'number', 'default' => 50 ],
// @TODO: [V4+] Convert this to the dynamic options like in search appearance so we can have backups when plugins are deactivated.
'postTypes' => [
'all' => [ 'type' => 'boolean', 'default' => true ],
'included' => [ 'type' => 'array', 'default' => [ 'post', 'page', 'product' ] ],
]
],
'html' => [
'enable' => [ 'type' => 'boolean', 'default' => true ],
'pageUrl' => [ 'type' => 'string', 'default' => '' ],
'postTypes' => [
'all' => [ 'type' => 'boolean', 'default' => true ],
'included' => [ 'type' => 'array', 'default' => [ 'post', 'page', 'product' ] ],
],
'taxonomies' => [
'all' => [ 'type' => 'boolean', 'default' => true ],
'included' => [ 'type' => 'array', 'default' => [ 'category', 'post_tag', 'product_cat', 'product_tag' ] ],
],
'sortOrder' => [ 'type' => 'string', 'default' => 'publish_date' ],
'sortDirection' => [ 'type' => 'string', 'default' => 'asc' ],
'publicationDate' => [ 'type' => 'boolean', 'default' => true ],
'compactArchives' => [ 'type' => 'boolean', 'default' => false ],
'advancedSettings' => [
'enable' => [ 'type' => 'boolean', 'default' => false ],
'nofollowLinks' => [ 'type' => 'boolean', 'default' => false ],
'excludePosts' => [ 'type' => 'array', 'default' => [] ],
'excludeTerms' => [ 'type' => 'array', 'default' => [] ]
]
],
],
'social' => [
'profiles' => [
'sameUsername' => [
'enable' => [ 'type' => 'boolean', 'default' => false ],
'username' => [ 'type' => 'string' ],
'included' => [ 'type' => 'array', 'default' => [ 'facebookPageUrl', 'twitterUrl', 'tiktokUrl', 'pinterestUrl', 'instagramUrl', 'youtubeUrl', 'linkedinUrl' ] ]
],
'urls' => [
'facebookPageUrl' => [ 'type' => 'string' ],
'twitterUrl' => [ 'type' => 'string' ],
'instagramUrl' => [ 'type' => 'string' ],
'tiktokUrl' => [ 'type' => 'string' ],
'pinterestUrl' => [ 'type' => 'string' ],
'youtubeUrl' => [ 'type' => 'string' ],
'linkedinUrl' => [ 'type' => 'string' ],
'tumblrUrl' => [ 'type' => 'string' ],
'yelpPageUrl' => [ 'type' => 'string' ],
'soundCloudUrl' => [ 'type' => 'string' ],
'wikipediaUrl' => [ 'type' => 'string' ],
'myspaceUrl' => [ 'type' => 'string' ],
'googlePlacesUrl' => [ 'type' => 'string' ],
'wordPressUrl' => [ 'type' => 'string' ],
'blueskyUrl' => [ 'type' => 'string' ],
'threadsUrl' => [ 'type' => 'string' ]
],
'additionalUrls' => [ 'type' => 'string' ]
],
'facebook' => [
'general' => [
'enable' => [ 'type' => 'boolean', 'default' => true ],
'defaultImageSourcePosts' => [ 'type' => 'string', 'default' => 'default' ],
'customFieldImagePosts' => [ 'type' => 'string' ],
'defaultImagePosts' => [ 'type' => 'string', 'default' => '' ],
'defaultImagePostsWidth' => [ 'type' => 'number', 'default' => '' ],
'defaultImagePostsHeight' => [ 'type' => 'number', 'default' => '' ],
'showAuthor' => [ 'type' => 'boolean', 'default' => true ],
'siteName' => [ 'type' => 'string', 'localized' => true, 'default' => '#site_title #separator_sa #tagline' ]
],
'homePage' => [
'image' => [ 'type' => 'string', 'default' => '' ],
'title' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
'description' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
'imageWidth' => [ 'type' => 'number', 'default' => '' ],
'imageHeight' => [ 'type' => 'number', 'default' => '' ],
'objectType' => [ 'type' => 'string', 'default' => 'website' ]
],
'advanced' => [
'enable' => [ 'type' => 'boolean', 'default' => false ],
'adminId' => [ 'type' => 'string', 'default' => '' ],
'appId' => [ 'type' => 'string', 'default' => '' ],
'authorUrl' => [ 'type' => 'string', 'default' => '' ],
'generateArticleTags' => [ 'type' => 'boolean', 'default' => false ],
'useKeywordsInTags' => [ 'type' => 'boolean', 'default' => true ],
'useCategoriesInTags' => [ 'type' => 'boolean', 'default' => true ],
'usePostTagsInTags' => [ 'type' => 'boolean', 'default' => true ]
]
],
'twitter' => [
'general' => [
'enable' => [ 'type' => 'boolean', 'default' => true ],
'useOgData' => [ 'type' => 'boolean', 'default' => true ],
'defaultCardType' => [ 'type' => 'string', 'default' => 'summary_large_image' ],
'defaultImageSourcePosts' => [ 'type' => 'string', 'default' => 'default' ],
'customFieldImagePosts' => [ 'type' => 'string' ],
'defaultImagePosts' => [ 'type' => 'string', 'default' => '' ],
'showAuthor' => [ 'type' => 'boolean', 'default' => true ],
'additionalData' => [ 'type' => 'boolean', 'default' => false ]
],
'homePage' => [
'image' => [ 'type' => 'string', 'default' => '' ],
'title' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
'description' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
'cardType' => [ 'type' => 'string', 'default' => 'summary' ]
],
]
],
'searchAppearance' => [
'global' => [
'separator' => [ 'type' => 'string', 'default' => '-' ],
'siteTitle' => [ 'type' => 'string', 'localized' => true, 'default' => '#site_title #separator_sa #tagline' ],
'metaDescription' => [ 'type' => 'string', 'localized' => true, 'default' => '#tagline' ],
'keywords' => [ 'type' => 'string', 'localized' => true ],
'schema' => [
'websiteName' => [ 'type' => 'string', 'default' => '#site_title' ],
'websiteAlternateName' => [ 'type' => 'string' ],
'siteRepresents' => [ 'type' => 'string', 'default' => 'organization' ],
'person' => [ 'type' => 'string' ],
'organizationName' => [ 'type' => 'string', 'default' => '#site_title' ],
'organizationDescription' => [ 'type' => 'string', 'default' => '#tagline' ],
'organizationLogo' => [ 'type' => 'string' ],
'personName' => [ 'type' => 'string' ],
'personLogo' => [ 'type' => 'string' ],
'phone' => [ 'type' => 'string' ],
'email' => [ 'type' => 'string' ],
'foundingDate' => [ 'type' => 'string' ],
'numberOfEmployees' => [
'isRange' => [ 'type' => 'boolean' ],
'from' => [ 'type' => 'number' ],
'to' => [ 'type' => 'number' ],
'number' => [ 'type' => 'number' ]
]
]
],
'advanced' => [
'globalRobotsMeta' => [
'default' => [ 'type' => 'boolean', 'default' => true ],
'noindex' => [ 'type' => 'boolean', 'default' => false ],
'nofollow' => [ 'type' => 'boolean', 'default' => false ],
'noindexPaginated' => [ 'type' => 'boolean', 'default' => true ],
'nofollowPaginated' => [ 'type' => 'boolean', 'default' => true ],
'noindexFeed' => [ 'type' => 'boolean', 'default' => true ],
'noarchive' => [ 'type' => 'boolean', 'default' => false ],
'noimageindex' => [ 'type' => 'boolean', 'default' => false ],
'notranslate' => [ 'type' => 'boolean', 'default' => false ],
'nosnippet' => [ 'type' => 'boolean', 'default' => false ],
'noodp' => [ 'type' => 'boolean', 'default' => false ],
'maxSnippet' => [ 'type' => 'number', 'default' => -1 ],
'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
],
'noIndexEmptyCat' => [ 'type' => 'boolean', 'default' => true ],
'removeStopWords' => [ 'type' => 'boolean', 'default' => false ],
'useKeywords' => [ 'type' => 'boolean', 'default' => false ],
'keywordsLooking' => [ 'type' => 'boolean', 'default' => true ],
'useCategoriesForMetaKeywords' => [ 'type' => 'boolean', 'default' => false ],
'useTagsForMetaKeywords' => [ 'type' => 'boolean', 'default' => false ],
'dynamicallyGenerateKeywords' => [ 'type' => 'boolean', 'default' => false ],
'pagedFormat' => [ 'type' => 'string', 'default' => '#separator_sa Page #page_number', 'localized' => true ],
'runShortcodes' => [ 'type' => 'boolean', 'default' => false ],
'crawlCleanup' => [
'enable' => [ 'type' => 'boolean', 'default' => false ],
'feeds' => [
'global' => [ 'type' => 'boolean', 'default' => true ],
'globalComments' => [ 'type' => 'boolean', 'default' => false ],
'staticBlogPage' => [ 'type' => 'boolean', 'default' => true ],
'authors' => [ 'type' => 'boolean', 'default' => true ],
'postComments' => [ 'type' => 'boolean', 'default' => false ],
'search' => [ 'type' => 'boolean', 'default' => false ],
'attachments' => [ 'type' => 'boolean', 'default' => false ],
'archives' => [
'all' => [ 'type' => 'boolean', 'default' => false ],
'included' => [ 'type' => 'array', 'default' => [] ],
],
'taxonomies' => [
'all' => [ 'type' => 'boolean', 'default' => false ],
'included' => [ 'type' => 'array', 'default' => [ 'category' ] ],
],
'atom' => [ 'type' => 'boolean', 'default' => false ],
'rdf' => [ 'type' => 'boolean', 'default' => false ],
'paginated' => [ 'type' => 'boolean', 'default' => false ]
]
],
'unwantedBots' => [
'all' => [ 'type' => 'boolean', 'default' => false ],
'settings' => [
'googleAdsBot' => [ 'type' => 'boolean', 'default' => false ],
'openAiGptBot' => [ 'type' => 'boolean', 'default' => false ],
'commonCrawlCcBot' => [ 'type' => 'boolean', 'default' => false ],
'googleGeminiVertexAiBots' => [ 'type' => 'boolean', 'default' => false ]
]
],
'searchCleanup' => [
'enable' => [ 'type' => 'boolean', 'default' => false ],
'settings' => [
'maxAllowedNumberOfChars' => [ 'type' => 'number', 'default' => 50 ],
'emojisAndSymbols' => [ 'type' => 'boolean', 'default' => false ],
'commonPatterns' => [ 'type' => 'boolean', 'default' => false ],
'redirectPrettyUrls' => [ 'type' => 'boolean', 'default' => false ],
'preventCrawling' => [ 'type' => 'boolean', 'default' => false ]
]
],
'blockArgs' => [
'enable' => [ 'type' => 'boolean', 'default' => false ],
'optimizeUtmParameters' => [ 'type' => 'boolean', 'default' => false ],
'logsRetention' => [ 'type' => 'string', 'default' => '{"label":"1 week","value":"week"}' ]
],
'removeCategoryBase' => [ 'type' => 'boolean', 'default' => false ]
],
'archives' => [
'author' => [
'show' => [ 'type' => 'boolean', 'default' => true ],
'title' => [ 'type' => 'string', 'localized' => true, 'default' => '#author_name #separator_sa #site_title' ],
'metaDescription' => [ 'type' => 'string', 'localized' => true, 'default' => '#author_bio' ],
'advanced' => [
'robotsMeta' => [
'default' => [ 'type' => 'boolean', 'default' => true ],
'noindex' => [ 'type' => 'boolean', 'default' => false ],
'nofollow' => [ 'type' => 'boolean', 'default' => false ],
'noarchive' => [ 'type' => 'boolean', 'default' => false ],
'noimageindex' => [ 'type' => 'boolean', 'default' => false ],
'notranslate' => [ 'type' => 'boolean', 'default' => false ],
'nosnippet' => [ 'type' => 'boolean', 'default' => false ],
'noodp' => [ 'type' => 'boolean', 'default' => false ],
'maxSnippet' => [ 'type' => 'number', 'default' => -1 ],
'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
],
'showDateInGooglePreview' => [ 'type' => 'boolean', 'default' => true ],
'showPostThumbnailInSearch' => [ 'type' => 'boolean', 'default' => true ],
'showMetaBox' => [ 'type' => 'boolean', 'default' => true ],
'keywords' => [ 'type' => 'string', 'localized' => true ]
]
],
'date' => [
'show' => [ 'type' => 'boolean', 'default' => true ],
'title' => [ 'type' => 'string', 'localized' => true, 'default' => '#archive_date #separator_sa #site_title' ],
'metaDescription' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
'advanced' => [
'robotsMeta' => [
'default' => [ 'type' => 'boolean', 'default' => true ],
'noindex' => [ 'type' => 'boolean', 'default' => false ],
'nofollow' => [ 'type' => 'boolean', 'default' => false ],
'noarchive' => [ 'type' => 'boolean', 'default' => false ],
'noimageindex' => [ 'type' => 'boolean', 'default' => false ],
'notranslate' => [ 'type' => 'boolean', 'default' => false ],
'nosnippet' => [ 'type' => 'boolean', 'default' => false ],
'noodp' => [ 'type' => 'boolean', 'default' => false ],
'maxSnippet' => [ 'type' => 'number', 'default' => -1 ],
'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
],
'showDateInGooglePreview' => [ 'type' => 'boolean', 'default' => true ],
'showPostThumbnailInSearch' => [ 'type' => 'boolean', 'default' => true ],
'showMetaBox' => [ 'type' => 'boolean', 'default' => true ],
'keywords' => [ 'type' => 'string', 'localized' => true ]
]
],
'search' => [
'show' => [ 'type' => 'boolean', 'default' => false ],
'title' => [ 'type' => 'string', 'localized' => true, 'default' => '#search_term #separator_sa #site_title' ],
'metaDescription' => [ 'type' => 'string', 'localized' => true, 'default' => '' ],
'advanced' => [
'robotsMeta' => [
'default' => [ 'type' => 'boolean', 'default' => false ],
'noindex' => [ 'type' => 'boolean', 'default' => true ],
'nofollow' => [ 'type' => 'boolean', 'default' => false ],
'noarchive' => [ 'type' => 'boolean', 'default' => false ],
'noimageindex' => [ 'type' => 'boolean', 'default' => false ],
'notranslate' => [ 'type' => 'boolean', 'default' => false ],
'nosnippet' => [ 'type' => 'boolean', 'default' => false ],
'noodp' => [ 'type' => 'boolean', 'default' => false ],
'maxSnippet' => [ 'type' => 'number', 'default' => -1 ],
'maxVideoPreview' => [ 'type' => 'number', 'default' => -1 ],
'maxImagePreview' => [ 'type' => 'string', 'default' => 'large' ]
],
'showDateInGooglePreview' => [ 'type' => 'boolean', 'default' => true ],
'showPostThumbnailInSearch' => [ 'type' => 'boolean', 'default' => true ],
'showMetaBox' => [ 'type' => 'boolean', 'default' => true ],
'keywords' => [ 'type' => 'string', 'localized' => true ]
]
]
]
],
'searchStatistics' => [
'postTypes' => [
'all' => [ 'type' => 'boolean', 'default' => true ],
'included' => [ 'type' => 'array', 'default' => [ 'post', 'page' ] ],
]
],
'tools' => [
'robots' => [
'enable' => [ 'type' => 'boolean', 'default' => false ],
'rules' => [ 'type' => 'array', 'default' => [] ],
'robotsDetected' => [ 'type' => 'boolean', 'default' => true ],
],
'importExport' => [
'backup' => [
'lastTime' => [ 'type' => 'string' ],
'data' => [ 'type' => 'string' ],
]
]
],
'deprecated' => [
'breadcrumbs' => [
'enable' => [ 'type' => 'boolean', 'default' => true ]
],
'searchAppearance' => [
'global' => [
'descriptionFormat' => [ 'type' => 'string' ],
'schema' => [
'enableSchemaMarkup' => [ 'type' => 'boolean', 'default' => true ]
]
],
'advanced' => [
'autogenerateDescriptions' => [ 'type' => 'boolean', 'default' => true ],
'runShortcodesInDescription' => [ 'type' => 'boolean', 'default' => true ], // TODO: Remove this in a future update.
'useContentForAutogeneratedDescriptions' => [ 'type' => 'boolean', 'default' => false ],
'excludePosts' => [ 'type' => 'array', 'default' => [] ],
'excludeTerms' => [ 'type' => 'array', 'default' => [] ],
'noPaginationForCanonical' => [ 'type' => 'boolean', 'default' => true ]
]
],
'sitemap' => [
'general' => [
'advancedSettings' => [
'dynamic' => [ 'type' => 'boolean', 'default' => true ]
]
]
]
],
'writingAssistant' => [
'postTypes' => [
'all' => [ 'type' => 'boolean', 'default' => true ],
'included' => [ 'type' => 'array', 'default' => [ 'post', 'page' ] ],
]
]
// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
];
/**
* The Construct method.
*
* @since 4.0.0
*
* @param string $optionsName An array of options.
*/
public function __construct( $optionsName = 'aioseo_options' ) {
$this->optionsName = $optionsName;
$this->init();
add_action( 'shutdown', [ $this, 'save' ] );
}
/**
* Initializes the options.
*
* @since 4.0.0
*
* @return void
*/
public function init() {
$this->setInitialDefaults();
add_action( 'init', [ $this, 'translateDefaults' ] );
$this->setDbOptions();
add_action( 'wp_loaded', [ $this, 'maybeFlushRewriteRules' ] );
}
/**
* Sets the DB options to the class after merging in new defaults and dropping unknown values.
*
* @since 4.0.14
*
* @return void
*/
public function setDbOptions() {
// Refactor options.
$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->defaultsMerged );
$dbOptions = $this->getDbOptions( $this->optionsName );
$options = array_replace_recursive(
$this->defaultsMerged,
$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
);
aioseo()->core->optionsCache->setOptions( $this->optionsName, apply_filters( 'aioseo_get_options', $options ) );
// Get the localized options.
$dbOptionsLocalized = get_option( $this->optionsName . '_localized' );
if ( empty( $dbOptionsLocalized ) ) {
$dbOptionsLocalized = [];
}
$this->localized = $dbOptionsLocalized;
}
/**
* Sets the initial defaults that can't be defined in the property because of PHP 5.4.
*
* @since 4.1.4
*
* @return void
*/
protected function setInitialDefaults() {
static $hasInitialized = false;
if ( $hasInitialized ) {
return;
}
$hasInitialized = true;
$this->defaults['searchAppearance']['global']['schema']['organizationLogo']['default'] = aioseo()->helpers->getSiteLogoUrl() ? aioseo()->helpers->getSiteLogoUrl() : '';
$this->defaults['advanced']['emailSummary']['recipients']['default'] = [
[
'email' => get_bloginfo( 'admin_email' ),
'frequency' => 'monthly',
]
];
}
/**
* For our defaults array, some options need to be translated, so we do that here.
*
* @since 4.0.0
*
* @return void
*/
public function translateDefaults() {
static $hasInitialized = false;
if ( $hasInitialized ) {
return;
}
$hasInitialized = true;
$default = sprintf( '{"label":"%1$s","value":"default"}', __( 'default', 'all-in-one-seo-pack' ) );
$this->defaults['sitemap']['general']['advancedSettings']['priority']['homePage']['priority']['default'] = $default;
$this->defaults['sitemap']['general']['advancedSettings']['priority']['homePage']['frequency']['default'] = $default;
$this->defaults['sitemap']['general']['advancedSettings']['priority']['postTypes']['priority']['default'] = $default;
$this->defaults['sitemap']['general']['advancedSettings']['priority']['postTypes']['frequency']['default'] = $default;
$this->defaults['sitemap']['general']['advancedSettings']['priority']['taxonomies']['priority']['default'] = $default;
$this->defaults['sitemap']['general']['advancedSettings']['priority']['taxonomies']['frequency']['default'] = $default;
$this->defaults['breadcrumbs']['homepageLabel']['default'] = __( 'Home', 'all-in-one-seo-pack' );
$this->defaults['breadcrumbs']['archiveFormat']['default'] = sprintf( '%1$s #breadcrumb_archive_post_type_name', __( 'Archives for', 'all-in-one-seo-pack' ) );
$this->defaults['breadcrumbs']['searchResultFormat']['default'] = sprintf( '%1$s \'#breadcrumb_search_string\'', __( 'Search Results for', 'all-in-one-seo-pack' ) );
$this->defaults['breadcrumbs']['errorFormat404']['default'] = __( '404 - Page Not Found', 'all-in-one-seo-pack' );
}
/**
* Sanitizes, then saves the options to the database.
*
* @since 4.0.0
*
* @param array $options An array of options to sanitize, then save.
* @return void
*/
public function sanitizeAndSave( $options ) {
$sitemapOptions = ! empty( $options['sitemap'] ) ? $options['sitemap'] : null;
$oldSitemapOptions = aioseo()->options->sitemap->all();
$generalSitemapOptions = ! empty( $options['sitemap']['general'] ) ? $options['sitemap']['general'] : null;
$oldGeneralSitemapOptions = aioseo()->options->sitemap->general->all();
$deprecatedGeneralSitemapOptions = ! empty( $options['deprecated']['sitemap']['general'] )
? $options['deprecated']['sitemap']['general']
: null;
$oldDeprecatedGeneralSitemapOptions = aioseo()->options->deprecated->sitemap->general->all();
$oldPhoneOption = aioseo()->options->searchAppearance->global->schema->phone;
$phoneNumberOptions = isset( $options['searchAppearance']['global']['schema']['phone'] )
? $options['searchAppearance']['global']['schema']['phone']
: null;
$oldHtmlSitemapUrl = aioseo()->options->sitemap->html->pageUrl;
$logsRetention = isset( $options['searchAppearance']['advanced']['blockArgs']['logsRetention'] ) ? $options['searchAppearance']['advanced']['blockArgs']['logsRetention'] : null;
$oldLogsRetention = aioseo()->options->searchAppearance->advanced->blockArgs->logsRetention;
// Remove category base.
$removeCategoryBase = isset( $options['searchAppearance']['advanced']['removeCategoryBase'] ) ? $options['searchAppearance']['advanced']['removeCategoryBase'] : null;
$removeCategoryBaseOld = aioseo()->options->searchAppearance->advanced->removeCategoryBase;
$options = $this->maybeRemoveUnfilteredHtmlFields( $options );
$this->init();
if ( ! is_array( $options ) ) {
return;
}
$this->sanitizeEmailSummary( $options );
// First, recursively replace the new options into the cached state.
// It's important we use the helper method since we want to replace populated arrays with empty ones if needed (when a setting was cleared out).
$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
$dbOptions = aioseo()->helpers->arrayReplaceRecursive(
$cachedOptions,
$this->addValueToValuesArray( $cachedOptions, $options, [], true )
);
// Now, we must also intersect both arrays to delete any individual keys that were unset.
// We must do this because, while arrayReplaceRecursive will update the values for keys or empty them out,
// it will keys that aren't present in the replacement array unaffected in the target array.
$dbOptions = aioseo()->helpers->arrayIntersectRecursive(
$dbOptions,
$this->addValueToValuesArray( $cachedOptions, $options, [], true ),
'value'
);
if ( isset( $options['social']['profiles']['additionalUrls'] ) ) {
$dbOptions['social']['profiles']['additionalUrls'] = preg_replace( '/\h/', "\n", (string) $options['social']['profiles']['additionalUrls'] );
}
$newOptions = ! empty( $options['sitemap']['html'] ) ? $options['sitemap']['html'] : null;
if ( ! empty( $newOptions ) && aioseo()->options->sitemap->html->enable ) {
$newOptions = ! empty( $options['sitemap']['html'] ) ? $options['sitemap']['html'] : null;
$pageUrl = wp_parse_url( $newOptions['pageUrl'] );
$path = ! empty( $pageUrl['path'] ) ? untrailingslashit( $pageUrl['path'] ) : '';
if ( $path ) {
$existingPage = get_page_by_path( $path, OBJECT );
if ( is_object( $existingPage ) ) {
// If the page exists, don't override the previous URL.
$options['sitemap']['html']['pageUrl'] = $oldHtmlSitemapUrl;
}
}
}
// Update the cache state.
aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );
// Update localized options.
update_option( $this->optionsName . '_localized', $this->localized );
// Finally, save the new values to the DB.
$this->save( true );
// If phone settings have changed, let's see if we need to dump the phone number notice.
if (
$phoneNumberOptions &&
$phoneNumberOptions !== $oldPhoneOption
) {
$notification = Models\Notification::getNotificationByName( 'v3-migration-schema-number' );
if ( $notification->exists() ) {
Models\Notification::deleteNotificationByName( 'v3-migration-schema-number' );
}
}
// If sitemap settings were changed, static files need to be regenerated.
if (
! empty( $deprecatedGeneralSitemapOptions ) &&
! empty( $generalSitemapOptions )
) {
if (
(
aioseo()->helpers->arraysDifferent( $oldGeneralSitemapOptions, $generalSitemapOptions ) ||
aioseo()->helpers->arraysDifferent( $oldDeprecatedGeneralSitemapOptions, $deprecatedGeneralSitemapOptions )
) &&
$generalSitemapOptions['advancedSettings']['enable'] &&
! $deprecatedGeneralSitemapOptions['advancedSettings']['dynamic']
) {
aioseo()->sitemap->scheduleRegeneration();
}
}
// Add or remove schedule for clearing crawl cleanup logs.
if ( ! empty( $logsRetention ) && $oldLogsRetention !== $logsRetention ) {
aioseo()->crawlCleanup->scheduleClearingLogs();
}
if ( ! empty( $sitemapOptions ) ) {
aioseo()->searchStatistics->sitemap->maybeSync( $oldSitemapOptions, $sitemapOptions );
}
if (
null !== $removeCategoryBase &&
$removeCategoryBase !== $removeCategoryBaseOld
) {
aioseo()->options->flushRewriteRules();
}
// This is required in order for the Pro options to be refreshed before they save data again.
$this->refresh();
}
/**
* Sanitizes the `emailSummary` option.
*
* @since 4.7.2
*
* @param array $options All options, passed by reference.
* @return void
*/
private function sanitizeEmailSummary( &$options ) {
foreach ( ( $options['advanced']['emailSummary']['recipients'] ?? [] ) as $k => &$recipient ) {
$recipient['email'] = is_email( $recipient['email'] );
// Remove empty emails.
if ( empty( $recipient['email'] ) ) {
unset( $options['advanced']['emailSummary']['recipients'][ $k ] );
continue;
}
// Remove duplicate emails with the same frequency.
foreach ( $options['advanced']['emailSummary']['recipients'] as $k2 => $recipient2 ) {
if (
$k !== $k2 &&
$recipient['email'] === $recipient2['email'] &&
$recipient['frequency'] === $recipient2['frequency']
) {
unset( $options['advanced']['emailSummary']['recipients'][ $k ] );
break;
}
}
}
}
/**
* If the user does not have access to unfiltered HTML, we need to remove them from saving.
*
* @since 4.0.0
*
* @param array $options An array of options.
* @return array An array of options.
*/
private function maybeRemoveUnfilteredHtmlFields( $options ) {
if ( current_user_can( 'unfiltered_html' ) ) {
return $options;
}
if (
! empty( $options['webmasterTools'] ) &&
isset( $options['webmasterTools']['miscellaneousVerification'] )
) {
unset( $options['webmasterTools']['miscellaneousVerification'] );
}
if (
! empty( $options['rssContent'] ) &&
isset( $options['rssContent']['before'] )
) {
unset( $options['rssContent']['before'] );
}
if (
! empty( $options['rssContent'] ) &&
isset( $options['rssContent']['after'] )
) {
unset( $options['rssContent']['after'] );
}
return $options;
}
/**
* Indicate we need to flush rewrite rules on next load.
*
* @since 4.0.17
*
* @return void
*/
public function flushRewriteRules() {
update_option( 'aioseo_flush_rewrite_rules_flag', true );
}
/**
* Flush rewrite rules if needed.
*
* @since 4.0.17
*
* @return void
*/
public function maybeFlushRewriteRules() {
if ( get_option( 'aioseo_flush_rewrite_rules_flag' ) ) {
flush_rewrite_rules();
delete_option( 'aioseo_flush_rewrite_rules_flag' );
}
}
} Options/InternalOptions.php 0000666 00000013205 15113050717 0012045 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Options;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Traits;
/**
* Class that holds all internal options for AIOSEO.
*
* @since 4.0.0
*/
class InternalOptions {
use Traits\Options;
/**
* Holds a list of all the possible deprecated options.
*
* @since 4.0.0
*
* @var array
*/
protected $allDeprecatedOptions = [
'autogenerateDescriptions',
'breadcrumbsEnable',
'descriptionFormat',
'enableSchemaMarkup',
'excludePosts',
'excludeTerms',
'googleAnalytics',
'noPaginationForCanonical',
'staticSitemap',
'staticVideoSitemap',
'useContentForAutogeneratedDescriptions'
];
/**
* All the default options.
*
* @since 4.0.0
*
* @var array
*/
protected $defaults = [
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
'internal' => [
'validLicenseKey' => [ 'type' => 'string' ],
'lastActiveVersion' => [ 'type' => 'string', 'default' => '0.0' ],
'migratedVersion' => [ 'type' => 'string' ],
'siteAnalysis' => [
'connectToken' => [ 'type' => 'string' ],
],
'headlineAnalysis' => [
'headlines' => [ 'type' => 'array', 'default' => [] ]
],
'wizard' => [ 'type' => 'string' ],
'category' => [ 'type' => 'string' ],
'categoryOther' => [ 'type' => 'string' ],
'deprecatedOptions' => [ 'type' => 'array', 'default' => [] ],
'searchStatistics' => [
'profile' => [ 'type' => 'array', 'default' => [] ],
'trustToken' => [ 'type' => 'string' ],
'rolling' => [ 'type' => 'string', 'default' => 'last28Days' ],
'site' => [
'verified' => [ 'type' => 'boolean', 'default' => false ],
'lastFetch' => [ 'type' => 'number', 'default' => 0 ]
],
'sitemap' => [
'list' => [ 'type' => 'array', 'default' => [] ],
'ignored' => [ 'type' => 'array', 'default' => [] ],
'lastFetch' => [ 'type' => 'number', 'default' => 0 ]
]
]
],
'integrations' => [
'semrush' => [
'accessToken' => [ 'type' => 'string' ],
'tokenType' => [ 'type' => 'string' ],
'expires' => [ 'type' => 'string' ],
'refreshToken' => [ 'type' => 'string' ]
]
],
'database' => [
'installedTables' => [ 'type' => 'string' ]
]
// phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
];
/**
* The Construct method.
*
* @since 4.0.0
*
* @param string $optionsName The options name.
*/
public function __construct( $optionsName = 'aioseo_options_internal' ) {
$this->optionsName = $optionsName;
$this->init();
add_action( 'shutdown', [ $this, 'save' ] );
}
/**
* Initializes the options.
*
* @since 4.0.0
*
* @return void
*/
protected function init() {
// Options from the DB.
$dbOptions = $this->getDbOptions( $this->optionsName );
// Refactor options.
$this->defaultsMerged = array_replace_recursive( $this->defaults, $this->defaultsMerged );
$options = array_replace_recursive(
$this->defaultsMerged,
$this->addValueToValuesArray( $this->defaultsMerged, $dbOptions )
);
aioseo()->core->optionsCache->setOptions( $this->optionsName, apply_filters( 'aioseo_get_options_internal', $options ) );
// Get the localized options.
$dbOptionsLocalized = get_option( $this->optionsName . '_localized' );
if ( empty( $dbOptionsLocalized ) ) {
$dbOptionsLocalized = [];
}
$this->localized = $dbOptionsLocalized;
}
/**
* Get all the deprecated options.
*
* @since 4.0.0
*
* @param bool $includeNamesAndValues Whether or not to include option names.
* @return array An array of deprecated options.
*/
public function getAllDeprecatedOptions( $includeNamesAndValues = false ) {
if ( ! $includeNamesAndValues ) {
return $this->allDeprecatedOptions;
}
$options = [];
foreach ( $this->allDeprecatedOptions as $deprecatedOption ) {
$options[] = [
'label' => ucwords( str_replace( '_', ' ', aioseo()->helpers->toSnakeCase( $deprecatedOption ) ) ),
'value' => $deprecatedOption,
'enabled' => in_array( $deprecatedOption, aioseo()->internalOptions->internal->deprecatedOptions, true )
];
}
return $options;
}
/**
* Sanitizes, then saves the options to the database.
*
* @since 4.0.0
*
* @param array $options An array of options to sanitize, then save.
* @return void
*/
public function sanitizeAndSave( $options ) {
if ( ! is_array( $options ) ) {
return;
}
// First, recursively replace the new options into the cached state.
// It's important we use the helper method since we want to replace populated arrays with empty ones if needed (when a setting was cleared out).
$cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName );
$dbOptions = aioseo()->helpers->arrayReplaceRecursive(
$cachedOptions,
$this->addValueToValuesArray( $cachedOptions, $options, [], true )
);
// Now, we must also intersect both arrays to delete any individual keys that were unset.
// We must do this because, while arrayReplaceRecursive will update the values for keys or empty them out,
// it will keys that aren't present in the replacement array unaffected in the target array.
$dbOptions = aioseo()->helpers->arrayIntersectRecursive(
$dbOptions,
$this->addValueToValuesArray( $cachedOptions, $options, [], true ),
'value'
);
// Update the cache state.
aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions );
// Update localized options.
update_option( $this->optionsName . '_localized', $this->localized );
// Finally, save the new values to the DB.
$this->save( true );
}
} Options/DynamicBackup.php 0000666 00000021020 15113050717 0011421 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Options;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the dynamic backup.
*
* @since 4.1.3
*/
class DynamicBackup {
/**
* A the name of the option to save dynamic backups to.
*
* @since 4.1.3
*
* @var string
*/
protected $optionsName = 'aioseo_dynamic_settings_backup';
/**
* The dynamic backup.
*
* @since 4.1.3
*
* @var array
*/
protected $backup = [];
/**
* Whether the backup should be updated.
*
* @since 4.1.3
*
* @var boolean
*/
protected $shouldBackup = false;
/**
* The option defaults.
*
* @since 4.1.3
*
* @var array
*/
protected $defaultOptions = [];
/**
* The public post types.
*
* @since 4.1.5
*
* @var array
*/
protected $postTypes = [];
/**
* The public taxonomies.
*
* @since 4.1.5
*
* @var array
*/
protected $taxonomies = [];
/**
* The public archives.
*
* @since 4.1.5
*
* @var array
*/
protected $archives = [];
/**
* Class constructor.
*
* @since 4.1.3
*/
public function __construct() {
add_action( 'wp_loaded', [ $this, 'init' ], 5000 );
add_action( 'shutdown', [ $this, 'updateBackup' ] );
}
/**
* Updates the backup after restoring options.
*
* @since 4.1.3
*
* @return void
*/
public function updateBackup() {
if ( $this->shouldBackup ) {
$this->shouldBackup = false;
$backup = aioseo()->dynamicOptions->convertOptionsToValues( $this->backup, 'value' );
update_option( $this->optionsName, wp_json_encode( $backup ), 'no' );
}
}
/**
* Checks whether data from the backup has to be restored.
*
* @since 4.1.3
*
* @return void
*/
public function init() {
$this->postTypes = wp_list_pluck( aioseo()->helpers->getPublicPostTypes( false, false, true ), 'name' );
$this->taxonomies = wp_list_pluck( aioseo()->helpers->getPublicTaxonomies( false, true ), 'name' );
$this->archives = wp_list_pluck( aioseo()->helpers->getPublicPostTypes( false, true, true ), 'name' );
$backup = json_decode( get_option( $this->optionsName ), true );
if ( empty( $backup ) ) {
update_option( $this->optionsName, '{}', 'no' );
return;
}
$this->backup = $backup;
$this->defaultOptions = aioseo()->dynamicOptions->getDefaults();
$this->restorePostTypes();
$this->restoreTaxonomies();
$this->restoreArchives();
}
/**
* Restores the dynamic Post Types options.
*
* @since 4.1.3
*
* @return void
*/
protected function restorePostTypes() {
foreach ( $this->postTypes as $postType ) {
// Restore the post types for Search Appearance.
if ( ! empty( $this->backup['postTypes'][ $postType ]['searchAppearance'] ) ) {
$this->restoreOptions( $this->backup['postTypes'][ $postType ]['searchAppearance'], [ 'searchAppearance', 'postTypes', $postType ] );
unset( $this->backup['postTypes'][ $postType ]['searchAppearance'] );
$this->shouldBackup = true;
}
// Restore the post types for Social Networks.
if ( ! empty( $this->backup['postTypes'][ $postType ]['social']['facebook'] ) ) {
$this->restoreOptions( $this->backup['postTypes'][ $postType ]['social']['facebook'], [ 'social', 'facebook', 'general', 'postTypes', $postType ] );
unset( $this->backup['postTypes'][ $postType ]['social']['facebook'] );
$this->shouldBackup = true;
}
}
}
/**
* Restores the dynamic Taxonomies options.
*
* @since 4.1.3
*
* @return void
*/
protected function restoreTaxonomies() {
foreach ( $this->taxonomies as $taxonomy ) {
// Restore the taxonomies for Search Appearance.
if ( ! empty( $this->backup['taxonomies'][ $taxonomy ]['searchAppearance'] ) ) {
$this->restoreOptions( $this->backup['taxonomies'][ $taxonomy ]['searchAppearance'], [ 'searchAppearance', 'taxonomies', $taxonomy ] );
unset( $this->backup['taxonomies'][ $taxonomy ]['searchAppearance'] );
$this->shouldBackup = true;
}
// Restore the taxonomies for Social Networks.
if ( ! empty( $this->backup['taxonomies'][ $taxonomy ]['social']['facebook'] ) ) {
$this->restoreOptions( $this->backup['taxonomies'][ $taxonomy ]['social']['facebook'], [ 'social', 'facebook', 'general', 'taxonomies', $taxonomy ] );
unset( $this->backup['taxonomies'][ $taxonomy ]['social']['facebook'] );
$this->shouldBackup = true;
}
}
}
/**
* Restores the dynamic Archives options.
*
* @since 4.1.3
*
* @return void
*/
protected function restoreArchives() {
foreach ( $this->archives as $postType ) {
// Restore the archives for Search Appearance.
if ( ! empty( $this->backup['archives'][ $postType ]['searchAppearance'] ) ) {
$this->restoreOptions( $this->backup['archives'][ $postType ]['searchAppearance'], [ 'searchAppearance', 'archives', $postType ] );
unset( $this->backup['archives'][ $postType ]['searchAppearance'] );
$this->shouldBackup = true;
}
}
}
/**
* Restores the backuped options.
*
* @since 4.1.3
*
* @param array $backupOptions The options to be restored.
* @param array $groups The group that the option should be restored to.
* @return void
*/
protected function restoreOptions( $backupOptions, $groups ) {
$defaultOptions = $this->defaultOptions;
foreach ( $groups as $group ) {
if ( ! isset( $defaultOptions[ $group ] ) ) {
return;
}
$defaultOptions = $defaultOptions[ $group ];
}
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
foreach ( $backupOptions as $setting => $value ) {
// Check if the option exists before proceeding. If not, it might be a group.
$type = $defaultOptions[ $setting ]['type'] ?? '';
if (
! $type &&
is_array( $value ) &&
aioseo()->helpers->isArrayAssociative( $value )
) {
$nextGroups = array_merge( $groups, [ $setting ] );
$this->restoreOptions( $backupOptions[ $setting ], $nextGroups );
continue;
}
// If we still can't find the option, it might be a group.
if ( ! $type ) {
continue;
}
foreach ( $groups as $group ) {
$dynamicOptions = $dynamicOptions->$group;
}
$dynamicOptions->$setting = $value;
}
}
/**
* Maybe backup the options if it has disappeared.
*
* @since 4.1.3
*
* @param array $newOptions An array of options to check.
* @return void
*/
public function maybeBackup( $newOptions ) {
$this->maybeBackupPostType( $newOptions );
$this->maybeBackupTaxonomy( $newOptions );
$this->maybeBackupArchives( $newOptions );
}
/**
* Maybe backup the Post Types.
*
* @since 4.1.3
*
* @param array $newOptions An array of options to check.
* @return void
*/
protected function maybeBackupPostType( $newOptions ) {
// Maybe backup the post types for Search Appearance.
foreach ( $newOptions['searchAppearance']['postTypes'] as $dynamicPostTypeName => $dynamicPostTypeSettings ) {
$found = in_array( $dynamicPostTypeName, $this->postTypes, true );
if ( ! $found ) {
$this->backup['postTypes'][ $dynamicPostTypeName ]['searchAppearance'] = $dynamicPostTypeSettings;
$this->shouldBackup = true;
}
}
// Maybe backup the post types for Social Networks.
foreach ( $newOptions['social']['facebook']['general']['postTypes'] as $dynamicPostTypeName => $dynamicPostTypeSettings ) {
$found = in_array( $dynamicPostTypeName, $this->postTypes, true );
if ( ! $found ) {
$this->backup['postTypes'][ $dynamicPostTypeName ]['social']['facebook'] = $dynamicPostTypeSettings;
$this->shouldBackup = true;
}
}
}
/**
* Maybe backup the Taxonomies.
*
* @since 4.1.3
*
* @param array $newOptions An array of options to check.
* @return void
*/
protected function maybeBackupTaxonomy( $newOptions ) {
// Maybe backup the taxonomies for Search Appearance.
foreach ( $newOptions['searchAppearance']['taxonomies'] as $dynamicTaxonomyName => $dynamicTaxonomySettings ) {
$found = in_array( $dynamicTaxonomyName, $this->taxonomies, true );
if ( ! $found ) {
$this->backup['taxonomies'][ $dynamicTaxonomyName ]['searchAppearance'] = $dynamicTaxonomySettings;
$this->shouldBackup = true;
}
}
}
/**
* Maybe backup the Archives.
*
* @since 4.1.3
*
* @param array $newOptions An array of options to check.
* @return void
*/
protected function maybeBackupArchives( $newOptions ) {
// Maybe backup the archives for Search Appearance.
foreach ( $newOptions['searchAppearance']['archives'] as $archiveName => $archiveSettings ) {
$found = in_array( $archiveName, $this->archives, true );
if ( ! $found ) {
$this->backup['archives'][ $archiveName ]['searchAppearance'] = $archiveSettings;
$this->shouldBackup = true;
}
}
}
} Sitemap/Root.php 0000666 00000043317 15113050717 0007616 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Determines which indexes should appear in the sitemap root index.
*
* @since 4.0.0
*/
class Root {
/**
* Returns the indexes for the sitemap root index.
*
* @since 4.0.0
*
* @return array The indexes.
*/
public function indexes() {
$indexes = [];
if ( 'general' !== aioseo()->sitemap->type ) {
$addonIndexes = aioseo()->addons->doAddonFunction( 'root', 'indexes' );
foreach ( $addonIndexes as $addonIndex ) {
if ( $addonIndex ) {
return $addonIndex;
}
}
return $indexes;
}
$filename = aioseo()->sitemap->filename;
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
$taxonomies = aioseo()->sitemap->helpers->includedTaxonomies();
$indexes = array_merge( $indexes, $this->getAdditionalIndexes() );
if ( $postTypes ) {
$postArchives = [];
foreach ( $postTypes as $postType ) {
$postIndexes = $this->buildIndexesPostType( $postType );
$indexes = array_merge( $indexes, $postIndexes );
if (
get_post_type_archive_link( $postType ) &&
aioseo()->dynamicOptions->noConflict()->searchAppearance->archives->has( $postType ) &&
(
aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->default ||
! aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->noindex
)
) {
$lastModifiedPostTime = aioseo()->sitemap->helpers->lastModifiedPostTime( $postType );
if ( $lastModifiedPostTime ) {
$postArchives[ $postType ] = $lastModifiedPostTime;
}
}
}
if ( ! empty( $postArchives ) ) {
usort( $postArchives, function( $date1, $date2 ) {
return $date1 < $date2 ? 1 : 0;
} );
$indexes[] = [
'loc' => aioseo()->helpers->localizedUrl( "/post-archive-$filename.xml" ),
'lastmod' => $postArchives[0],
'count' => count( $postArchives )
];
}
}
if ( $taxonomies ) {
foreach ( $taxonomies as $taxonomy ) {
$indexes = array_merge( $indexes, $this->buildIndexesTaxonomy( $taxonomy ) );
}
}
$postsTable = aioseo()->core->db->db->posts;
if (
aioseo()->sitemap->helpers->lastModifiedPost() &&
aioseo()->options->sitemap->general->author &&
aioseo()->options->searchAppearance->archives->author->show &&
(
aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default ||
! aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex
) &&
(
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
)
) {
$usersTable = aioseo()->core->db->db->users; // We get the table name from WPDB since multisites share the same table.
$authorPostTypes = aioseo()->sitemap->helpers->getAuthorPostTypes();
$implodedPostTypes = aioseo()->helpers->implodeWhereIn( $authorPostTypes, true );
$result = aioseo()->core->db->execute(
"SELECT count(*) as amountOfAuthors FROM
(
SELECT u.ID FROM {$usersTable} as u
INNER JOIN {$postsTable} as p ON u.ID = p.post_author
WHERE p.post_status = 'publish' AND p.post_type IN ( {$implodedPostTypes} )
GROUP BY u.ID
) as x",
true
)->result();
if ( ! empty( $result[0]->amountOfAuthors ) ) {
$indexes = array_merge( $indexes, $this->buildAuthorIndexes( (int) $result[0]->amountOfAuthors ) );
}
}
if (
aioseo()->sitemap->helpers->lastModifiedPost() &&
aioseo()->options->sitemap->general->date &&
aioseo()->options->searchAppearance->archives->date->show &&
(
aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default ||
! aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex
) &&
(
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
)
) {
$result = aioseo()->core->db->execute(
"SELECT count(*) as amountOfUrls FROM (
SELECT post_date
FROM {$postsTable}
WHERE post_type = 'post' AND post_status = 'publish'
GROUP BY
YEAR(post_date),
MONTH(post_date)
LIMIT 50000
) as dates",
true
)->result();
$indexes[] = $this->buildIndex( 'date', $result[0]->amountOfUrls );
}
if (
aioseo()->helpers->isWooCommerceActive() &&
in_array( 'product_attributes', aioseo()->sitemap->helpers->includedTaxonomies(), true )
) {
$productAttributes = aioseo()->sitemap->content->productAttributes( true );
if ( ! empty( $productAttributes ) ) {
$indexes[] = $this->buildIndex( 'product_attributes', $productAttributes );
}
}
if ( isset( aioseo()->standalone->buddyPress->sitemap ) ) {
$indexes = array_merge( $indexes, aioseo()->standalone->buddyPress->sitemap->indexes() );
}
return apply_filters( 'aioseo_sitemap_indexes', array_filter( $indexes ) );
}
/**
* Returns the additional page indexes.
*
* @since 4.2.1
*
* @return array
*/
private function getAdditionalIndexes() {
$additionalPages = [];
if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
$additionalPages = array_map( 'json_decode', aioseo()->options->sitemap->general->additionalPages->pages );
$additionalPages = array_filter( $additionalPages, function( $additionalPage ) {
return ! empty( $additionalPage->url );
} );
}
$entries = [];
foreach ( $additionalPages as $additionalPage ) {
$entries[] = [
'loc' => $additionalPage->url,
'lastmod' => aioseo()->sitemap->helpers->lastModifiedAdditionalPage( $additionalPage ),
'changefreq' => $additionalPage->frequency->value,
'priority' => $additionalPage->priority->value,
'isTimezone' => true
];
}
if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
$entries = apply_filters( 'aioseo_sitemap_additional_pages', $entries );
}
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
$shouldIncludeHomepage = 'posts' === get_option( 'show_on_front' ) || ! in_array( 'page', $postTypes, true );
if ( ! $shouldIncludeHomepage && ! count( $entries ) ) {
return [];
}
$indexes = $this->buildAdditionalIndexes( $entries, $shouldIncludeHomepage );
return $indexes;
}
/**
* Builds a given index.
*
* @since 4.0.0
*
* @param string $indexName The index name.
* @param integer $amountOfUrls The amount of URLs in the index.
* @return array The index.
*/
private function buildIndex( $indexName, $amountOfUrls ) {
$filename = aioseo()->sitemap->filename;
return [
'loc' => aioseo()->helpers->localizedUrl( "/$indexName-$filename.xml" ),
'lastmod' => aioseo()->sitemap->helpers->lastModifiedPostTime(),
'count' => $amountOfUrls
];
}
/**
* Builds the additional pages index.
*
* @since 4.0.0
*
* @param array $entries The additional pages.
* @param bool $shouldIncludeHomepage Whether or not the homepage should be included.
* @return array The indexes.
*/
private function buildAdditionalIndexes( $entries, $shouldIncludeHomepage ) {
if ( $shouldIncludeHomepage ) {
$entries[] = [
'loc' => home_url(),
'lastmod' => aioseo()->sitemap->helpers->lastModifiedPostTime()
];
}
if ( empty( $entries ) ) {
return [];
}
$filename = aioseo()->sitemap->filename;
$chunks = aioseo()->sitemap->helpers->chunkEntries( $entries );
$indexes = [];
for ( $i = 0; $i < count( $chunks ); $i++ ) {
$chunk = array_values( $chunks[ $i ] );
$indexNumber = 1 < count( $chunks ) ? $i + 1 : '';
$index = [
'loc' => aioseo()->helpers->localizedUrl( "/addl-$filename$indexNumber.xml" ),
'lastmod' => ! empty( $chunk[0]['lastmod'] ) ? aioseo()->helpers->dateTimeToIso8601( $chunk[0]['lastmod'] ) : '',
'count' => count( $chunks[ $i ] )
];
$indexes[] = $index;
}
return $indexes;
}
/**
* Builds the author archive indexes.
*
* @since 4.3.1
*
* @param integer $amountOfAuthors The amount of author archives.
* @return array The indexes.
*/
private function buildAuthorIndexes( $amountOfAuthors ) {
if ( ! $amountOfAuthors ) {
return [];
}
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
$filename = aioseo()->sitemap->filename;
$chunks = $amountOfAuthors / aioseo()->sitemap->linksPerIndex;
if ( $chunks < 1 ) {
$chunks = 1;
}
$indexes = [];
for ( $i = 0; $i < $chunks; $i++ ) {
$indexNumber = 1 < $chunks ? $i + 1 : '';
$usersTableName = aioseo()->core->db->db->users; // We get the table name from WPDB since multisites share the same table.
$lastModified = aioseo()->core->db->start( "$usersTableName as u", true )
->select( 'MAX(p.post_modified_gmt) as lastModified' )
->join( 'posts as p', 'u.ID = p.post_author' )
->where( 'p.post_status', 'publish' )
->whereIn( 'p.post_type', $postTypes )
->groupBy( 'u.ID' )
->orderBy( 'lastModified DESC' )
->limit( aioseo()->sitemap->linksPerIndex, $i * aioseo()->sitemap->linksPerIndex )
->run()
->result();
$lastModified = ! empty( $lastModified[0]->lastModified ) ? aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->lastModified ) : '';
$index = [
'loc' => aioseo()->helpers->localizedUrl( "/author-$filename$indexNumber.xml" ),
'lastmod' => $lastModified,
'count' => $i + 1 === $chunks ? $amountOfAuthors % aioseo()->sitemap->linksPerIndex : aioseo()->sitemap->linksPerIndex
];
$indexes[] = $index;
}
return $indexes;
}
/**
* Builds indexes for all eligible posts of a given post type.
*
* @since 4.0.0
*
* @param string $postType The post type.
* @return array The indexes.
*/
private function buildIndexesPostType( $postType ) {
$prefix = aioseo()->core->db->prefix;
$postsTable = $prefix . 'posts';
$aioseoPostsTable = $prefix . 'aioseo_posts';
$termRelationshipsTable = $prefix . 'term_relationships';
$termTaxonomyTable = $prefix . 'term_taxonomy';
$termsTable = $prefix . 'terms';
$linksPerIndex = aioseo()->sitemap->linksPerIndex;
if ( 'attachment' === $postType && 'disabled' !== aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls ) {
return [];
}
$excludedPostIds = [];
$excludedTermIds = aioseo()->sitemap->helpers->excludedTerms();
if ( ! empty( $excludedTermIds ) ) {
$excludedTermIds = explode( ', ', $excludedTermIds );
$excludedPostIds = aioseo()->core->db->start( 'term_relationships' )
->select( 'object_id' )
->whereIn( 'term_taxonomy_id', $excludedTermIds )
->run()
->result();
$excludedPostIds = array_map( function( $post ) {
return $post->object_id;
}, $excludedPostIds );
}
if ( 'page' === $postType ) {
$isStaticHomepage = 'page' === get_option( 'show_on_front' );
if ( $isStaticHomepage ) {
$blogPageId = (int) get_option( 'page_for_posts' );
$excludedPostIds[] = $blogPageId;
}
}
$whereClause = '';
$excludedPostsString = aioseo()->sitemap->helpers->excludedPosts();
if ( ! empty( $excludedPostsString ) ) {
$excludedPostIds = array_merge( $excludedPostIds, explode( ', ', $excludedPostsString ) );
}
if ( ! empty( $excludedPostIds ) ) {
$implodedPostIds = aioseo()->helpers->implodeWhereIn( $excludedPostIds, true );
$whereClause = "AND p.ID NOT IN ( $implodedPostIds )";
}
if (
apply_filters( 'aioseo_sitemap_woocommerce_exclude_hidden_products', true ) &&
aioseo()->helpers->isWooCommerceActive() &&
'product' === $postType
) {
$whereClause .= " AND p.ID NOT IN (
SELECT CONVERT(tr.object_id, unsigned) AS object_id
FROM {$termRelationshipsTable} AS tr
JOIN {$termTaxonomyTable} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
JOIN {$termsTable} AS t ON tt.term_id = t.term_id
WHERE t.name = 'exclude-from-catalog'
)";
}
// Include the blog page in the posts post type unless manually excluded.
$blogPageId = (int) get_option( 'page_for_posts' );
if (
$blogPageId &&
! in_array( $blogPageId, $excludedPostIds, true ) &&
'post' === $postType
) {
$whereClause .= " OR `p`.`ID` = $blogPageId ";
}
$posts = aioseo()->core->db->execute(
aioseo()->core->db->db->prepare(
"SELECT ID, post_modified_gmt
FROM (
SELECT @row := @row + 1 AS rownum, ID, post_modified_gmt
FROM (
SELECT p.ID, ap.priority, p.post_modified_gmt
FROM {$postsTable} AS p
LEFT JOIN {$aioseoPostsTable} AS ap ON p.ID = ap.post_id
WHERE p.post_status = %s
AND p.post_type = %s
AND p.post_password = ''
AND (ap.robots_noindex IS NULL OR ap.robots_default = 1 OR ap.robots_noindex = 0)
{$whereClause}
ORDER BY ap.priority DESC, p.post_modified_gmt DESC
) AS x
CROSS JOIN (SELECT @row := 0) AS vars
ORDER BY post_modified_gmt DESC
) AS y
WHERE rownum = 1 OR rownum % %d = 1;",
[
'attachment' === $postType ? 'inherit' : 'publish',
$postType,
$linksPerIndex
]
),
true
)->result();
$totalPosts = aioseo()->core->db->execute(
aioseo()->core->db->db->prepare(
"SELECT COUNT(*) as count
FROM {$postsTable} as p
LEFT JOIN {$aioseoPostsTable} as ap ON p.ID = ap.post_id
WHERE p.post_status = %s
AND p.post_type = %s
AND p.post_password = ''
AND (ap.robots_noindex IS NULL OR ap.robots_default = 1 OR ap.robots_noindex = 0)
{$whereClause}
",
[
'attachment' === $postType ? 'inherit' : 'publish',
$postType
]
),
true
)->result();
if ( $posts ) {
$indexes = [];
$filename = aioseo()->sitemap->filename;
$postCount = count( $posts );
for ( $i = 0; $i < $postCount; $i++ ) {
$indexNumber = 0 !== $i && 1 < $postCount ? $i + 1 : '';
$indexes[] = [
'loc' => aioseo()->helpers->localizedUrl( "/$postType-$filename$indexNumber.xml" ),
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $posts[ $i ]->post_modified_gmt ),
'count' => $linksPerIndex
];
}
// We need to update the count of the last index since it won't necessarily be the same as the links per index.
$indexes[ count( $indexes ) - 1 ]['count'] = $totalPosts[0]->count - ( $linksPerIndex * ( $postCount - 1 ) );
return $indexes;
}
if ( ! $posts ) {
$addonsPosts = aioseo()->addons->doAddonFunction( 'root', 'buildIndexesPostType', [ $postType ] );
foreach ( $addonsPosts as $addonPosts ) {
if ( $addonPosts ) {
$posts = $addonPosts;
break;
}
}
}
if ( ! $posts ) {
return [];
}
return $this->buildIndexes( $postType, $posts );
}
/**
* Builds indexes for all eligible terms of a given taxonomy.
*
* @since 4.0.0
*
* @param string $taxonomy The taxonomy.
* @return array The indexes.
*/
private function buildIndexesTaxonomy( $taxonomy ) {
$terms = aioseo()->sitemap->content->terms( $taxonomy, [ 'root' => true ] );
if ( ! $terms ) {
$addonsTerms = aioseo()->addons->doAddonFunction( 'root', 'buildIndexesTaxonomy', [ $taxonomy ] );
foreach ( $addonsTerms as $addonTerms ) {
if ( $addonTerms ) {
$terms = $addonTerms;
break;
}
}
}
if ( ! $terms ) {
return [];
}
return $this->buildIndexes( $taxonomy, $terms );
}
/**
* Builds indexes for a given type.
*
* Acts as a helper function for buildIndexesPostTypes() and buildIndexesTaxonomies().
*
* @since 4.0.0
*
* @param string $name The name of the object parent.
* @param array $entries The sitemap entries.
* @return array The indexes.
*/
public function buildIndexes( $name, $entries ) {
$filename = aioseo()->sitemap->filename;
$chunks = aioseo()->sitemap->helpers->chunkEntries( $entries );
$indexes = [];
for ( $i = 0; $i < count( $chunks ); $i++ ) {
$chunk = array_values( $chunks[ $i ] );
$indexNumber = 0 !== $i && 1 < count( $chunks ) ? $i + 1 : '';
$index = [
'loc' => aioseo()->helpers->localizedUrl( "/$name-$filename$indexNumber.xml" ),
'count' => count( $chunks[ $i ] )
];
if ( isset( $entries[0]->ID ) ) {
$ids = array_map( function( $post ) {
return $post->ID;
}, $chunk );
$ids = implode( "', '", $ids );
$lastModified = null;
if ( ! apply_filters( 'aioseo_sitemap_lastmod_disable', false ) ) {
$lastModified = aioseo()->core->db
->start( aioseo()->core->db->db->posts . ' as p', true )
->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
->whereRaw( "( `p`.`ID` IN ( '$ids' ) )" )
->run()
->result();
}
if ( ! empty( $lastModified[0]->last_modified ) ) {
$index['lastmod'] = aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->last_modified );
}
$indexes[] = $index;
continue;
}
$termIds = [];
foreach ( $chunk as $term ) {
$termIds[] = $term->term_id;
}
$termIds = implode( "', '", $termIds );
$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
$lastModified = null;
if ( ! apply_filters( 'aioseo_sitemap_lastmod_disable', false ) ) {
$lastModified = aioseo()->core->db
->start( aioseo()->core->db->db->posts . ' as p', true )
->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
->whereRaw( "
( `p`.`ID` IN
(
SELECT CONVERT(`tr`.`object_id`, unsigned)
FROM `$termRelationshipsTable` as tr
WHERE `tr`.`term_taxonomy_id` IN ( '$termIds' )
)
)" )
->run()
->result();
}
if ( ! empty( $lastModified[0]->last_modified ) ) {
$index['lastmod'] = aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->last_modified );
}
$indexes[] = $index;
}
return $indexes;
}
} Sitemap/Priority.php 0000666 00000016577 15113050717 0010524 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Determines the priority/frequency.
*
* @since 4.0.0
*/
class Priority {
/**
* Whether the advanced settings are enabled for the sitemap.
*
* @since 4.0.0
*
* @var boolean
*/
private static $advanced;
/**
* The global priority for the page type.
*
* @since 4.0.0
*
* @var boolean
*/
private static $globalPriority = [];
/**
* The global frequency for the page type.
*
* @since 4.0.0
*
* @var boolean
*/
private static $globalFrequency = [];
/**
* Whether or not we have grouped our settings.
*
* @since 4.0.0
*
* @var array
*/
private static $grouped = [];
/**
* The current object type priority.
*
* @since 4.0.0
*
* @var array
*/
private static $objectTypePriority = [];
/**
* The current object type frequency.
*
* @since 4.0.0
*
* @var array
*/
private static $objectTypeFrequency = [];
/**
* Returns the sitemap priority for a given page.
*
* @since 4.0.0
*
* @param string $pageType The type of page (e.g. homepage, blog, post, taxonomies, etc.).
* @param \stdClass|bool $object The post/term object (optional).
* @param string $objectType The post/term object type (optional).
* @return float The priority.
*/
public function priority( $pageType, $object = false, $objectType = '' ) {
// Store setting values in static properties so that we can cache them.
// Otherwise this has a significant impact on the load time of the sitemap.
if ( ! self::$advanced ) {
self::$advanced = aioseo()->options->sitemap->general->advancedSettings->enable;
}
if ( ! isset( self::$globalPriority[ $pageType . $objectType ] ) ) {
$options = aioseo()->options->noConflict();
$pageTypeConditional = 'date' === $pageType ? 'archive' : $pageType;
self::$globalPriority[ $pageType . $objectType ] = self::$advanced && $options->sitemap->general->advancedSettings->priority->has( $pageTypeConditional )
? json_decode( $options->sitemap->general->advancedSettings->priority->$pageTypeConditional->priority )
: false;
}
if ( ! isset( self::$grouped[ $pageType . $objectType ] ) ) {
$options = aioseo()->options->noConflict();
self::$grouped[ $pageType . $objectType ] = self::$advanced &&
$options->sitemap->general->advancedSettings->priority->has( $pageType ) &&
$options->sitemap->general->advancedSettings->priority->$pageType->has( 'grouped' )
? $options->sitemap->general->advancedSettings->priority->$pageType->grouped
: true;
}
if ( empty( self::$grouped[ $pageType . $objectType ] ) && self::$advanced ) {
if ( ! isset( self::$objectTypePriority[ $pageType . $objectType ] ) ) {
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
self::$objectTypePriority[ $pageType . $objectType ] = $dynamicOptions->sitemap->priority->has( $pageType ) && $dynamicOptions->sitemap->priority->$pageType->has( $objectType )
? json_decode( $dynamicOptions->sitemap->priority->$pageType->$objectType->priority )
: false;
}
}
$priority = $this->defaultPriority( $pageType );
if ( self::$globalPriority[ $pageType . $objectType ] ) {
$defaultValue = ! self::$grouped[ $pageType . $objectType ] &&
self::$advanced &&
! empty( self::$objectTypePriority[ $pageType . $objectType ] )
? self::$objectTypePriority[ $pageType . $objectType ]
: self::$globalPriority[ $pageType . $objectType ];
$priority = 'default' === $defaultValue->value ? $priority : $defaultValue->value;
}
return $priority;
}
/**
* Returns the sitemap frequency for a given page.
*
* @since 4.0.0
*
* @param string $pageType The type of page (e.g. homepage, blog, post, taxonomies, etc.).
* @param \stdClass|bool $object The post/term object (optional).
* @param string $objectType The post/term object type (optional).
* @return float The frequency.
*/
public function frequency( $pageType, $object = false, $objectType = '' ) {
// Store setting values in static properties so that we can cache them.
// Otherwise this has a significant impact on the load time of the sitemap.
if ( ! self::$advanced ) {
self::$advanced = aioseo()->options->sitemap->general->advancedSettings->enable;
}
if ( ! isset( self::$globalFrequency[ $pageType . $objectType ] ) ) {
$options = aioseo()->options->noConflict();
$pageTypeConditional = 'date' === $pageType ? 'archive' : $pageType;
self::$globalFrequency[ $pageType . $objectType ] = self::$advanced && $options->sitemap->general->advancedSettings->priority->has( $pageTypeConditional )
? json_decode( $options->sitemap->general->advancedSettings->priority->$pageTypeConditional->frequency )
: false;
}
if ( ! isset( self::$grouped[ $pageType . $objectType ] ) ) {
$options = aioseo()->options->noConflict();
self::$grouped[ $pageType . $objectType ] = self::$advanced &&
$options->sitemap->general->advancedSettings->priority->has( $pageType ) &&
$options->sitemap->general->advancedSettings->priority->$pageType->has( 'grouped' )
? $options->sitemap->general->advancedSettings->priority->$pageType->grouped
: true;
}
if ( empty( self::$grouped[ $pageType . $objectType ] ) && self::$advanced ) {
if ( ! isset( self::$objectTypeFrequency[ $pageType . $objectType ] ) ) {
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
self::$objectTypeFrequency[ $pageType . $objectType ] = $dynamicOptions->sitemap->priority->has( $pageType ) && $dynamicOptions->sitemap->priority->$pageType->has( $objectType )
? json_decode( $dynamicOptions->sitemap->priority->$pageType->$objectType->frequency )
: false;
}
}
$frequency = $this->defaultFrequency( $pageType );
if ( self::$globalFrequency[ $pageType . $objectType ] ) {
$defaultValue = ! self::$grouped[ $pageType . $objectType ] &&
self::$advanced &&
! empty( self::$objectTypeFrequency[ $pageType . $objectType ] )
? self::$objectTypeFrequency[ $pageType . $objectType ]
: self::$globalFrequency[ $pageType . $objectType ];
$frequency = 'default' === $defaultValue->value ? $frequency : $defaultValue->value;
}
return $frequency;
}
/**
* Returns the default priority for the page.
*
* @since 4.0.0
*
* @param string $pageType The type of page.
* @return float The default priority.
*/
private function defaultPriority( $pageType ) {
$defaults = [
'homePage' => 1.0,
'blog' => 0.9,
'sitemap' => 0.8,
'postTypes' => 0.7,
'archive' => 0.5,
'author' => 0.3,
'taxonomies' => 0.3,
'other' => 0.5
];
if ( array_key_exists( $pageType, $defaults ) ) {
return $defaults[ $pageType ];
}
return $defaults['other'];
}
/**
* Returns the default frequency for the page.
*
* @since 4.0.0
*
* @param string $pageType The type of page.
* @return float The default frequency.
*/
private function defaultFrequency( $pageType ) {
$defaults = [
'homePage' => 'always',
'sitemap' => 'hourly',
'blog' => 'daily',
'postTypes' => 'weekly',
'author' => 'weekly',
'archive' => 'monthly',
'taxonomies' => 'monthly',
'other' => 'weekly'
];
if ( array_key_exists( $pageType, $defaults ) ) {
return $defaults[ $pageType ];
}
return $defaults['other'];
}
} Sitemap/Image/Image.php 0000666 00000022536 15113050717 0010737 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap\Image;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Determines which images are included in a post/term.
*
* @since 4.0.0
*/
class Image {
/**
* The image scan action name.
*
* @since 4.0.13
*
* @var string
*/
private $imageScanAction = 'aioseo_image_sitemap_scan';
/**
* The supported image extensions.
*
* @since 4.2.2
*
* @var array[string]
*/
public $supportedExtensions = [
'gif',
'heic',
'jpeg',
'jpg',
'png',
'svg',
'webp',
'ico'
];
/**
* The post object.
*
* @since 4.2.7
*
* @var \WP_Post
*/
private $post = null;
/**
* Class constructor.
*
* @since 4.0.5
*/
public function __construct() {
// Column may not have been created yet.
if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'image_scan_date' ) ) {
return;
}
// NOTE: This needs to go above the is_admin check in order for it to run at all.
add_action( $this->imageScanAction, [ $this, 'scanPosts' ] );
// Don't schedule a scan if we are not in the admin.
if ( ! is_admin() ) {
return;
}
if ( wp_doing_ajax() || wp_doing_cron() ) {
return;
}
// Don't schedule a scan if an importer or the V3 migration is running.
// We'll do our scans there.
if (
aioseo()->importExport->isImportRunning() ||
aioseo()->migration->isMigrationRunning()
) {
return;
}
// Action Scheduler hooks.
add_action( 'init', [ $this, 'scheduleScan' ], 3001 );
}
/**
* Schedules the image sitemap scan.
*
* @since 4.0.5
*
* @return void
*/
public function scheduleScan() {
if (
! aioseo()->options->sitemap->general->enable ||
aioseo()->sitemap->helpers->excludeImages()
) {
return;
}
aioseo()->actionScheduler->scheduleSingle( $this->imageScanAction, 10 );
}
/**
* Scans posts for images.
*
* @since 4.0.5
*
* @return void
*/
public function scanPosts() {
if (
! aioseo()->options->sitemap->general->enable ||
aioseo()->sitemap->helpers->excludeImages()
) {
return;
}
$postsPerScan = apply_filters( 'aioseo_image_sitemap_posts_per_scan', 10 );
$postTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
$posts = aioseo()->core->db
->start( aioseo()->core->db->db->posts . ' as p', true )
->select( '`p`.`ID`, `p`.`post_type`, `p`.`post_content`, `p`.`post_excerpt`, `p`.`post_modified_gmt`' )
->leftJoin( 'aioseo_posts as ap', '`ap`.`post_id` = `p`.`ID`' )
->whereRaw( '( `ap`.`id` IS NULL OR `p`.`post_modified_gmt` > `ap`.`image_scan_date` OR `ap`.`image_scan_date` IS NULL )' )
->whereRaw( "`p`.`post_status` IN ( 'publish', 'inherit' )" )
->whereRaw( "`p`.`post_type` IN ( '$postTypes' )" )
->limit( $postsPerScan )
->run()
->result();
if ( ! $posts ) {
aioseo()->actionScheduler->scheduleSingle( $this->imageScanAction, 15 * MINUTE_IN_SECONDS, [], true );
return;
}
foreach ( $posts as $post ) {
$this->scanPost( $post );
}
aioseo()->actionScheduler->scheduleSingle( $this->imageScanAction, 30, [], true );
}
/**
* Returns the image entries for a given post.
*
* @since 4.0.0
*
* @param \WP_Post|int $post The post object or ID.
* @return void
*/
public function scanPost( $post ) {
if ( is_numeric( $post ) ) {
$post = get_post( $post );
}
$this->post = $post;
if ( ! empty( $post->post_password ) ) {
$this->updatePost( $post->ID );
return;
}
if ( 'attachment' === $post->post_type ) {
if ( ! wp_attachment_is( 'image', $post->ID ) ) {
$this->updatePost( $post->ID );
return;
}
$image = $this->buildEntries( [ $post->ID ] );
$this->updatePost( $post->ID, $image );
return;
}
$images = $this->extract();
$images = $this->removeImageDimensions( $images );
$images = apply_filters( 'aioseo_sitemap_images', $images, $post );
// Limit to a 1,000 URLs, in accordance to Google's specifications.
$images = array_slice( $images, 0, 1000 );
$this->updatePost( $post->ID, $this->buildEntries( $images ) );
}
/**
* Returns the image entries for a given term.
*
* @since 4.0.0
*
* @param \WP_Term $term The term object.
* @return array The image entries.
*/
public function term( $term ) {
if ( aioseo()->sitemap->helpers->excludeImages() ) {
return [];
}
$id = get_term_meta( $term->term_id, 'thumbnail_id', true );
if ( ! $id ) {
return [];
}
return $this->buildEntries( [ $id ] );
}
/**
* Builds the image entries.
*
* @since 4.0.0
*
* @param array $images The images, consisting of attachment IDs or external URLs.
* @return array The image entries.
*/
private function buildEntries( $images ) {
$entries = [];
foreach ( $images as $image ) {
$idOrUrl = $this->getImageIdOrUrl( $image );
$imageUrl = is_numeric( $idOrUrl ) ? wp_get_attachment_url( $idOrUrl ) : $idOrUrl;
$imageUrl = aioseo()->sitemap->helpers->formatUrl( $imageUrl );
if ( ! $imageUrl || ! preg_match( $this->getImageExtensionRegexPattern(), (string) $imageUrl ) ) {
continue;
}
// If the image URL is not external, make it relative.
// This is important for users who scan their sites in a local/staging environment and then
// push the data to production.
if ( ! aioseo()->helpers->isExternalUrl( $imageUrl ) ) {
$imageUrl = aioseo()->helpers->makeUrlRelative( $imageUrl );
}
$entries[ $idOrUrl ] = [ 'image:loc' => $imageUrl ];
}
return array_values( $entries );
}
/**
* Returns the ID of the image if it's hosted on the site. Otherwise it returns the external URL.
*
* @since 4.1.3
*
* @param int|string $image The attachment ID or URL.
* @return int|string The attachment ID or URL.
*/
private function getImageIdOrUrl( $image ) {
if ( is_numeric( $image ) ) {
return $image;
}
$attachmentId = false;
if ( aioseo()->helpers->isValidAttachment( $image ) ) {
$attachmentId = aioseo()->helpers->attachmentUrlToPostId( $image );
}
return $attachmentId ? $attachmentId : $image;
}
/**
* Extracts all image URls and IDs from the post.
*
* @since 4.0.0
*
* @return array The image URLs and IDs.
*/
private function extract() {
$images = [];
if ( has_post_thumbnail( $this->post ) ) {
$images[] = get_the_post_thumbnail_url( $this->post );
}
// Get the galleries here before doShortcodes() runs below to prevent buggy behaviour.
// WordPress is supposed to only return the attached images but returns a different result if the shortcode has no valid attributes, so we need to grab them manually.
$images = array_merge( $images, $this->getPostGalleryImages() );
// Now, get the remaining images from image tags in the post content.
$parsedPostContent = do_blocks( $this->post->post_content );
$parsedPostContent = aioseo()->helpers->doShortcodes( $parsedPostContent, true, $this->post->ID );
$parsedPostContent = preg_replace( '/\s\s+/u', ' ', (string) trim( $parsedPostContent ) ); // Trim both internal and external whitespace.
// Get the images from any third-party plugins/themes that are active.
$thirdParty = new ThirdParty( $this->post, $parsedPostContent );
$images = array_merge( $images, $thirdParty->extract() );
preg_match_all( '#<(amp-)?img[^>]+src="([^">]+)"#', (string) $parsedPostContent, $matches );
foreach ( $matches[2] as $url ) {
$images[] = aioseo()->helpers->makeUrlAbsolute( $url );
}
return array_unique( $images );
}
/**
* Returns all images from WP Core post galleries.
*
* @since 4.2.2
*
* @return array[string] The image URLs.
*/
private function getPostGalleryImages() {
$images = [];
$galleries = get_post_galleries( $this->post, false );
foreach ( $galleries as $gallery ) {
foreach ( $gallery['src'] as $imageUrl ) {
$images[] = $imageUrl;
}
}
// Now, get rid of them so that we don't process the shortcodes again.
$regex = get_shortcode_regex( [ 'gallery' ] );
$this->post->post_content = preg_replace( "/$regex/i", '', (string) $this->post->post_content );
return $images;
}
/**
* Removes image dimensions from the slug.
*
* @since 4.0.0
*
* @param array $urls The image URLs.
* @return array $preparedUrls The formatted image URLs.
*/
private function removeImageDimensions( $urls ) {
$preparedUrls = [];
foreach ( $urls as $url ) {
$preparedUrls[] = aioseo()->helpers->removeImageDimensions( $url );
}
return array_unique( array_filter( $preparedUrls ) );
}
/**
* Stores the image data for a given post in our DB table.
*
* @since 4.0.5
*
* @param int $postId The post ID.
* @param array $images The images.
* @return void
*/
private function updatePost( $postId, $images = [] ) {
$post = \AIOSEO\Plugin\Common\Models\Post::getPost( $postId );
$meta = $post->exists() ? [] : aioseo()->migration->meta->getMigratedPostMeta( $postId );
$meta['post_id'] = $postId;
$meta['images'] = ! empty( $images ) ? $images : null;
$meta['image_scan_date'] = gmdate( 'Y-m-d H:i:s' );
$post->set( $meta );
$post->save();
}
/**
* Returns the image extension regex pattern.
*
* @since 4.2.2
*
* @return string
*/
public function getImageExtensionRegexPattern() {
static $pattern;
if ( null !== $pattern ) {
return $pattern;
}
$pattern = '/http.*\.(' . implode( '|', $this->supportedExtensions ) . ')$/i';
return $pattern;
}
} Sitemap/Image/ThirdParty.php 0000666 00000017641 15113050717 0012010 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap\Image;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Holds all code to extract images from third-party content.
*
* @since 4.2.2
*/
class ThirdParty {
/**
* The post object.
*
* @since 4.2.2
*
* @var \WP_Post
*/
private $post;
/**
* The parsed post content.
* The post object holds the unparsed content as we need that for Divi.
*
* @since 4.2.5
*
* @var string
*/
private $parsedPostContent;
/**
* The image URLs and IDs.
*
* @since 4.2.2
*
* @var array[mixed]
*/
private $images = [];
/**
* Divi shortcodes.
*
* @since 4.2.3
*
* @var string[]
*/
private $shortcodes = [
'et_pb_section',
'et_pb_column',
'et_pb_row',
'et_pb_image',
'et_pb_gallery',
'et_pb_accordion',
'et_pb_accordion_item',
'et_pb_counters',
'et_pb_blurb',
'et_pb_cta',
'et_pb_code',
'et_pb_contact_form',
'et_pb_divider',
'et_pb_filterable_portfolio',
'et_pb_map',
'et_pb_number_counter',
'et_pb_post_slider',
'et_pb_pricing_tables',
'et_pb_pricing_table',
'et_pb_shop',
'et_pb_slider',
'et_pb_slide',
'et_pb_tabs',
'et_pb_tab',
'et_pb_text',
'et_pb_video',
'et_pb_audio',
'et_pb_blog',
'et_pb_circle_counter',
'et_pb_comments',
'et_pb_countdown_timer',
'et_pb_signup',
'et_pb_login',
'et_pb_menu',
'et_pb_team_member',
'et_pb_post_nav',
'et_pb_post_title',
'et_pb_search',
'et_pb_sidebar',
'et_pb_social_media_follow',
'et_pb_social_media_follow_network',
'et_pb_testimonial',
'et_pb_toggle',
'et_pb_video_slider',
'et_pb_video_slider_item',
];
/**
* Class constructor.
*
* @since 4.2.2
*
* @param \WP_Post $post The post object.
* @param string $parsedPostContent The parsed post content.
*/
public function __construct( $post, $parsedPostContent ) {
$this->post = $post;
$this->parsedPostContent = $parsedPostContent;
}
/**
* Extracts the images from third-party content.
*
* @since 4.2.2
*
* @return array[mixed] The image URLs and IDs.
*/
public function extract() {
$integrations = [
'acf',
'divi',
'nextGen',
'wooCommerce',
'kadenceBlocks'
];
foreach ( $integrations as $integration ) {
$this->{$integration}();
}
return $this->images;
}
/**
* Extracts image URLs from ACF fields.
*
* @since 4.2.2
*
* @return void
*/
private function acf() {
if ( ! class_exists( 'ACF' ) || ! function_exists( 'get_fields' ) ) {
return;
}
$fields = get_fields( $this->post->ID );
if ( ! $fields ) {
return;
}
$images = $this->acfHelper( $fields );
$this->images = array_merge( $this->images, $images );
}
/**
* Helper function for acf().
*
* @since 4.2.2
*
* @param array $fields The ACF fields.
* @return array[string] The image URLs or IDs.
*/
private function acfHelper( $fields ) {
$images = [];
foreach ( $fields as $value ) {
if ( is_array( $value ) ) {
// Recursively loop over grouped fields.
// We continue on since arrays aren't necessarily groups and might also simply aLready contain the value we're looking for.
$images = array_merge( $images, $this->acfHelper( $value ) );
if ( isset( $value['type'] ) && 'image' !== strtolower( $value['type'] ) ) {
$images[] = $value['url'];
}
continue;
}
// Capture the value if it's an image URL, but not the default thumbnail from ACF.
if ( is_string( $value ) && preg_match( aioseo()->sitemap->image->getImageExtensionRegexPattern(), (string) $value ) && ! preg_match( '/media\/default\.png$/i', (string) $value ) ) {
$images[] = $value;
continue;
}
// Capture the value if it's a numeric image ID, but make sure it's not an array of random field object properties.
if (
is_numeric( $value ) &&
! isset( $fields['ID'] ) &&
! isset( $fields['thumbnail'] )
) {
$images[] = $value;
}
}
return $images;
}
/**
* Extracts images from Divi shortcodes.
*
* @since 4.1.8
*
* @return void
*/
private function divi() {
if ( ! defined( 'ET_BUILDER_VERSION' ) ) {
return;
}
$urls = [];
$regex = implode( '|', array_map( 'preg_quote', $this->shortcodes ) );
preg_match_all(
"/\[($regex)(?![\w-])([^\]\/]*(?:\/(?!\])[^\]\/]*)*?)(?:(\/)\]|\](?:([^\[]*+(?:\[(?!\/\2\])[^\[]*+)*+)\[\/\2\])?)(\]?)/i",
(string) $this->post->post_content,
$matches,
PREG_SET_ORDER
);
foreach ( $matches as $shortcode ) {
$attributes = shortcode_parse_atts( $shortcode[0] );
if ( ! empty( $attributes['src'] ) ) {
$urls[] = $attributes['src'];
}
if ( ! empty( $attributes['image_src'] ) ) {
$urls[] = $attributes['image_src'];
}
if ( ! empty( $attributes['image_url'] ) ) {
$urls[] = $attributes['image_url'];
}
if ( ! empty( $attributes['portrait_url'] ) ) {
$urls[] = $attributes['portrait_url'];
}
if ( ! empty( $attributes['image'] ) ) {
$urls[] = $attributes['image'];
}
if ( ! empty( $attributes['background_image'] ) ) {
$urls[] = $attributes['background_image'];
}
if ( ! empty( $attributes['logo'] ) ) {
$urls[] = $attributes['logo'];
}
if ( ! empty( $attributes['gallery_ids'] ) ) {
$attachmentIds = explode( ',', $attributes['gallery_ids'] );
foreach ( $attachmentIds as $attachmentId ) {
$urls[] = wp_get_attachment_url( $attachmentId );
}
}
}
$this->images = array_merge( $this->images, $urls );
}
/**
* Extracts the image IDs of more advanced NextGen Pro gallerlies like the Mosaic and Thumbnail Grid.
*
* @since 4.2.5
*
* @return void
*/
private function nextGen() {
if ( ! defined( 'NGG_PLUGIN_BASENAME' ) && ! defined( 'NGG_PRO_PLUGIN_BASENAME' ) ) {
return;
}
preg_match_all( '/data-image-id=\"([0-9]*)\"/i', (string) $this->parsedPostContent, $imageIds );
if ( ! empty( $imageIds[1] ) ) {
$this->images = array_merge( $this->images, $imageIds[1] );
}
// For this specific check, we only want to parse blocks and do not want to run shortcodes because some NextGen blocks (e.g. Mosaic) are parsed into shortcodes.
// And after parsing the shortcodes, the attributes we're looking for are gone.
$contentWithBlocksParsed = do_blocks( $this->post->post_content );
$imageIds = [];
preg_match_all( '/\[ngg.*src="galleries" ids="(.*?)".*\]/i', (string) $contentWithBlocksParsed, $shortcodes );
if ( empty( $shortcodes[1] ) ) {
return;
}
foreach ( $shortcodes[1] as $shortcode ) {
$galleryIds = explode( ',', $shortcode[0] );
foreach ( $galleryIds as $galleryId ) {
global $nggdb;
$galleryImageIds = $nggdb->get_ids_from_gallery( $galleryId );
if ( empty( $galleryImageIds ) ) {
continue;
}
foreach ( $galleryImageIds as $galleryImageId ) {
$image = $nggdb->find_image( $galleryImageId );
if ( ! empty( $image ) ) {
$imageIds[] = $image->get_permalink();
}
}
}
}
$this->images = array_merge( $this->images, $imageIds );
}
/**
* Extracts the image IDs of WooCommerce product galleries.
*
* @since 4.1.2
*
* @return void
*/
private function wooCommerce() {
if ( ! aioseo()->helpers->isWooCommerceActive() || 'product' !== $this->post->post_type ) {
return;
}
$productImageIds = get_post_meta( $this->post->ID, '_product_image_gallery', true );
if ( ! $productImageIds ) {
return;
}
$productImageIds = explode( ',', $productImageIds );
$this->images = array_merge( $this->images, $productImageIds );
}
/**
* Extracts the image IDs of Kadence Block galleries.
*
* @since 4.4.5
*
* @return void
*/
private function kadenceBlocks() {
if ( ! defined( 'KADENCE_BLOCKS_VERSION' ) ) {
return [];
}
$blocks = aioseo()->helpers->parseBlocks( $this->post );
foreach ( $blocks as $block ) {
if ( 'kadence/advancedgallery' === $block['blockName'] && ! empty( $block['attrs']['ids'] ) ) {
$this->images = array_merge( $this->images, $block['attrs']['ids'] );
}
}
}
} Sitemap/Xsl.php 0000666 00000012230 15113050717 0007427 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Serves stylesheets for sitemaps.
*
* @since 4.2.1
*/
class Xsl {
/**
* Generates the XSL stylesheet for the current sitemap.
*
* @since 4.2.1
*
* @return void
*/
public function generate() {
aioseo()->sitemap->headers();
$charset = aioseo()->helpers->getCharset();
$sitemapUrl = wp_get_referer();
$sitemapPath = aioseo()->helpers->getPermalinkPath( $sitemapUrl );
// Figure out which sitemap we're serving.
preg_match( '/\/(.*?)-?sitemap([0-9]*)\.xml/', (string) $sitemapPath, $sitemapInfo );
$sitemapName = ! empty( $sitemapInfo[1] ) ? strtoupper( $sitemapInfo[1] ) : '';
// Remove everything after ? from sitemapPath to avoid caching issues.
$sitemapPath = wp_parse_url( $sitemapPath, PHP_URL_PATH ) ?: '';
// These variables are used in the XSL file.
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
$advanced = aioseo()->options->sitemap->general->advancedSettings->enable;
$excludeImages = aioseo()->sitemap->helpers->excludeImages();
$sitemapParams = aioseo()->helpers->getParametersFromUrl( $sitemapUrl );
$xslParams = aioseo()->core->cache->get( 'aioseo_sitemap_' . aioseo()->sitemap->requestParser->cleanSlug( $sitemapPath ) );
// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! empty( $sitemapInfo[1] ) ) {
switch ( $sitemapInfo[1] ) {
case 'addl':
$sitemapName = __( 'Additional Pages', 'all-in-one-seo-pack' );
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$excludeImages = true;
break;
case 'post-archive':
$sitemapName = __( 'Post Archive', 'all-in-one-seo-pack' );
break;
case 'bp-activity':
case 'bp-group':
case 'bp-member':
$bpFakePostTypes = aioseo()->standalone->buddyPress->getFakePostTypes();
$labels = array_column( wp_list_filter( $bpFakePostTypes, [ 'name' => $sitemapInfo[1] ] ), 'label' );
$sitemapName = ! empty( $labels[0] ) ? $labels[0] : $sitemapName;
break;
case 'product_attributes':
$sitemapName = __( 'Product Attributes', 'all-in-one-seo-pack' );
break;
default:
if ( post_type_exists( $sitemapInfo[1] ) ) {
$postTypeObject = get_post_type_object( $sitemapInfo[1] );
$sitemapName = $postTypeObject->labels->singular_name;
}
if ( taxonomy_exists( $sitemapInfo[1] ) ) {
$taxonomyObject = get_taxonomy( $sitemapInfo[1] );
$sitemapName = $taxonomyObject->labels->singular_name;
}
break;
}
}
$currentPage = ! empty( $sitemapInfo[2] ) ? (int) $sitemapInfo[2] : 1;
// Translators: 1 - The sitemap name, 2 - The current page.
$title = sprintf( __( '%1$s Sitemap %2$s', 'all-in-one-seo-pack' ), $sitemapName, $currentPage > 1 ? $currentPage : '' );
$title = trim( $title );
echo '<?xml version="1.0" encoding="' . esc_attr( $charset ) . '"?>';
include_once AIOSEO_DIR . '/app/Common/Views/sitemap/xsl/default.php';
exit;
}
/**
* Save the data to use in the XSL.
*
* @since 4.1.5
*
* @param string $fileName The sitemap file name.
* @param array $entries The sitemap entries.
* @param int $total The total sitemap entries count.
* @return void
*/
public function saveXslData( $fileName, $entries, $total ) {
$counts = [];
$datetime = [];
$dateFormat = get_option( 'date_format' );
$timeFormat = get_option( 'time_format' );
$entries = aioseo()->sitemap->helpers->decodeSitemapEntries( $entries );
foreach ( $entries as $index ) {
$url = ! empty( $index['guid'] ) ? $index['guid'] : $index['loc'];
if ( ! empty( $index['count'] ) && aioseo()->options->sitemap->general->linksPerIndex !== (int) $index['count'] ) {
$counts[ $url ] = $index['count'];
}
if ( ! empty( $index['lastmod'] ) || ! empty( $index['publicationDate'] ) || ! empty( $index['pubDate'] ) ) {
$date = ! empty( $index['lastmod'] ) ? $index['lastmod'] : ( ! empty( $index['publicationDate'] ) ? $index['publicationDate'] : $index['pubDate'] );
$isTimezone = ! empty( $index['isTimezone'] ) && $index['isTimezone'];
$datetime[ $url ] = [
'date' => $isTimezone ? date_i18n( $dateFormat, strtotime( $date ) ) : get_date_from_gmt( $date, $dateFormat ),
'time' => $isTimezone ? date_i18n( $timeFormat, strtotime( $date ) ) : get_date_from_gmt( $date, $timeFormat )
];
}
}
$data = [
'counts' => $counts,
'datetime' => $datetime,
'pagination' => [
'showing' => count( $entries ),
'total' => $total
]
];
// Set a high expiration date so we still have the cache for static sitemaps.
aioseo()->core->cache->update( 'aioseo_sitemap_' . $fileName, $data, MONTH_IN_SECONDS );
}
/**
* Retrieve the data to use on the XSL.
*
* @since 4.2.1
*
* @param string $fileName The sitemap file name.
* @return array The XSL data for the given file name.
*/
public function getXslData( $fileName ) {
return aioseo()->core->cache->get( 'aioseo_sitemap_' . $fileName );
}
} Sitemap/RequestParser.php 0000666 00000016473 15113050717 0011503 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Parses the current request and checks whether we need to serve a sitemap or a stylesheet.
*
* @since 4.2.1
*/
class RequestParser {
/**
* The cleaned slug of the current request.
*
* @since 4.2.1
*
* @var string
*/
public $slug;
/**
* Whether we've checked if the page needs to be redirected.
*
* @since 4.2.3
*
* @var bool
*/
protected $checkedForRedirects = false;
/**
* CLass constructor.
*
* @since 4.2.1
*/
public function __construct() {
if ( is_admin() ) {
return;
}
add_action( 'parse_request', [ $this, 'checkRequest' ] );
}
/**
* Checks whether we need to serve a sitemap or related stylesheet.
*
* @since 4.2.1
*
* @param \WP $wp The main WordPress environment instance.
* @return void
*/
public function checkRequest( $wp ) {
$this->slug = $wp->request ?? $this->cleanSlug( $wp->request );
if ( ! $this->slug && isset( $_SERVER['REQUEST_URI'] ) ) {
// We must fallback to the REQUEST URI in case the site uses plain permalinks.
$this->slug = $this->cleanSlug( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
}
if ( ! $this->slug ) {
return;
}
// Check if we need to remove the trailing slash or redirect another sitemap URL like "wp-sitemap.xml".
$this->maybeRedirect();
$this->checkForXsl();
if ( aioseo()->options->sitemap->general->enable ) {
$this->checkForGeneralSitemap();
}
if ( aioseo()->options->sitemap->rss->enable ) {
$this->checkForRssSitemap();
}
}
/**
* Cleans the slug of the current request before we use it.
*
* @since 4.2.3
*
* @param string $slug The slug.
* @return string The cleaned slug.
*/
public function cleanSlug( $slug ) {
$slug = strtolower( $slug );
$slug = aioseo()->helpers->unleadingSlashIt( $slug );
$slug = untrailingslashit( $slug );
return $slug;
}
/**
* Checks whether the general XML sitemap needs to be served.
*
* @since 4.2.1
*
* @return void
*/
private function checkForGeneralSitemap() {
$fileName = aioseo()->sitemap->helpers->filename( 'general' );
$indexesEnabled = aioseo()->options->sitemap->general->indexes;
if ( ! $indexesEnabled ) {
// If indexes are disabled, check for the root index.
if ( preg_match( "/^{$fileName}\.xml(\.gz)?$/i", (string) $this->slug, $match ) ) {
$this->setContext( 'general', $fileName );
aioseo()->sitemap->generate();
}
return;
}
// First, check for the root index.
if ( preg_match( "/^{$fileName}\.xml(\.gz)?$/i", (string) $this->slug, $match ) ) {
$this->setContext( 'general', $fileName );
aioseo()->sitemap->generate();
return;
}
if (
// Now, check for the other indexes.
preg_match( "/^(?P<objectName>.+)-{$fileName}\.xml(\.gz)?$/i", (string) $this->slug, $match ) ||
preg_match( "/^(?P<objectName>.+)-{$fileName}(?P<pageNumber>\d+)\.xml(\.gz)?$/i", (string) $this->slug, $match )
) {
$pageNumber = ! empty( $match['pageNumber'] ) ? $match['pageNumber'] : 0;
$this->setContext( 'general', $fileName, $match['objectName'], $pageNumber );
aioseo()->sitemap->generate();
}
}
/**
* Checks whether the RSS sitemap needs to be served.
*
* @since 4.2.1
*
* @return void
*/
private function checkForRssSitemap() {
if ( ! preg_match( '/^sitemap(\.latest)?\.rss$/i', (string) $this->slug, $match ) ) {
return;
}
$this->setContext( 'rss' );
aioseo()->sitemap->generate();
}
/**
* Checks if we need to serve a stylesheet.
*
* @since 4.2.1
*
* @return void
*/
protected function checkForXsl() {
// Trim off the URL params.
$newSlug = preg_replace( '/\?.*$/', '', (string) $this->slug );
if ( preg_match( '/^default-sitemap\.xsl$/i', (string) $newSlug ) ) {
aioseo()->sitemap->xsl->generate();
}
}
/**
* Sets the context for the requested sitemap.
*
* @since 4.2.1
*
* @param string $type The sitemap type (e.g. "general" or "rss").
* @param string $fileName The sitemap filename.
* @param string $indexName The index name ("root" or an object name like "post", "page", "post_tag", etc.).
* @param int $pageNumber The index number.
* @return void|never
*/
public function setContext( $type, $fileName = 'sitemap', $indexName = 'root', $pageNumber = 0 ) {
$indexesEnabled = aioseo()->options->sitemap->{$type}->indexes;
aioseo()->sitemap->type = $type;
aioseo()->sitemap->filename = $fileName;
aioseo()->sitemap->indexes = $indexesEnabled;
aioseo()->sitemap->indexName = $indexName;
aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->{$type}->linksPerIndex <= 50000 ? aioseo()->options->sitemap->{$type}->linksPerIndex : 50000;
aioseo()->sitemap->pageNumber = $pageNumber >= 1 ? $pageNumber - 1 : 0;
aioseo()->sitemap->offset = aioseo()->sitemap->linksPerIndex * aioseo()->sitemap->pageNumber;
aioseo()->sitemap->isStatic = false;
}
/**
* Redirects or alters the current request if:
* 1. The request includes our deprecated "aiosp_sitemap_path" URL param.
* 2. The request is for one of our sitemaps, but has a trailing slash.
* 3. The request is for the first index of a type, but has a page number.
* 4. The request is for a sitemap from WordPress Core/other plugin.
*
* @since 4.2.1
*/
protected function maybeRedirect() {
if ( $this->checkedForRedirects ) {
return;
}
$requestUri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
if ( ! $requestUri ) {
return;
}
$this->checkedForRedirects = true;
// The request includes our deprecated "aiosp_sitemap_path" URL param.
if ( preg_match( '/^\/\?aiosp_sitemap_path=root/i', (string) $requestUri ) ) {
wp_safe_redirect( home_url( 'sitemap.xml' ) );
exit;
}
// The request is for one of our sitemaps, but has a trailing slash.
if ( preg_match( '/\/(.*sitemap[0-9]*?\.xml(\.gz)?|.*sitemap(\.latest)?\.rss)\/$/i', (string) $requestUri ) ) {
wp_safe_redirect( home_url() . untrailingslashit( $requestUri ) );
exit;
}
// The request is for the first index of a type, but has a page number.
if ( preg_match( '/.*sitemap(0|1){1}?\.xml(\.gz)?$/i', (string) $requestUri ) ) {
$pathWithoutNumber = preg_replace( '/(.*sitemap)(0|1){1}?(\.xml(\.gz)?)$/i', '$1$3', $requestUri );
wp_safe_redirect( home_url() . $pathWithoutNumber );
exit;
}
// The request is for a sitemap from WordPress Core/other plugin, but the general sitemap is enabled.
if ( ! aioseo()->options->sitemap->general->enable ) {
return;
}
$sitemapPatterns = [
'general' => [
'sitemap\.txt',
'sitemaps\.xml',
'sitemap-xml\.xml',
'sitemap[0-9]+\.xml',
'sitemap(|[-_\/])?index[0-9]*\.xml',
'wp-sitemap\.xml',
],
'rss' => [
'rss[0-9]*\.xml',
]
];
$addonSitemapPatterns = aioseo()->addons->doAddonFunction( 'helpers', 'getOtherSitemapPatterns' );
if ( ! empty( $addonSitemapPatterns ) ) {
$sitemapPatterns = array_merge( $sitemapPatterns, $addonSitemapPatterns );
}
foreach ( $sitemapPatterns as $type => $patterns ) {
foreach ( $patterns as $pattern ) {
if ( preg_match( "/^$pattern$/i", (string) $this->slug ) ) {
wp_safe_redirect( aioseo()->sitemap->helpers->getUrl( $type ) );
exit;
}
}
}
}
} Sitemap/Content.php 0000666 00000074527 15113050717 0010314 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
/**
* Determines which content should be included in the sitemap.
*
* @since 4.0.0
*/
class Content {
/**
* Returns the entries for the requested sitemap.
*
* @since 4.0.0
*
* @return array The sitemap entries.
*/
public function get() {
if ( ! in_array( aioseo()->sitemap->type, [ 'general', 'rss' ], true ) || ! $this->isEnabled() ) {
return [];
}
if ( 'rss' === aioseo()->sitemap->type ) {
return $this->rss();
}
if ( 'general' !== aioseo()->sitemap->type ) {
return [];
}
$indexesEnabled = aioseo()->options->sitemap->general->indexes;
if ( ! $indexesEnabled ) {
if ( 'root' === aioseo()->sitemap->indexName ) {
// If indexes are disabled, throw all entries together into one big file.
return $this->nonIndexed();
}
return [];
}
if ( 'root' === aioseo()->sitemap->indexName ) {
return aioseo()->sitemap->root->indexes();
}
// Check if requested index has a dedicated method.
$methodName = aioseo()->helpers->dashesToCamelCase( aioseo()->sitemap->indexName );
if ( method_exists( $this, $methodName ) ) {
return $this->$methodName();
}
// Check if requested index is a registered post type.
if ( in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
return $this->posts( aioseo()->sitemap->indexName );
}
// Check if requested index is a registered taxonomy.
if (
in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedTaxonomies(), true ) &&
'product_attributes' !== aioseo()->sitemap->indexName
) {
return $this->terms( aioseo()->sitemap->indexName );
}
if (
aioseo()->helpers->isWooCommerceActive() &&
in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedTaxonomies(), true ) &&
'product_attributes' === aioseo()->sitemap->indexName
) {
return $this->productAttributes();
}
return [];
}
/**
* Returns the total entries number for the requested sitemap.
*
* @since 4.1.5
*
* @return int The total entries number.
*/
public function getTotal() {
if ( ! in_array( aioseo()->sitemap->type, [ 'general', 'rss' ], true ) || ! $this->isEnabled() ) {
return 0;
}
if ( 'rss' === aioseo()->sitemap->type ) {
return count( $this->rss() );
}
if ( 'general' !== aioseo()->sitemap->type ) {
return 0;
}
$indexesEnabled = aioseo()->options->sitemap->general->indexes;
if ( ! $indexesEnabled ) {
if ( 'root' === aioseo()->sitemap->indexName ) {
// If indexes are disabled, throw all entries together into one big file.
return count( $this->nonIndexed() );
}
return 0;
}
if ( 'root' === aioseo()->sitemap->indexName ) {
return count( aioseo()->sitemap->root->indexes() );
}
// Check if requested index has a dedicated method.
$methodName = aioseo()->helpers->dashesToCamelCase( aioseo()->sitemap->indexName );
if ( method_exists( $this, $methodName ) ) {
$res = $this->$methodName();
return ! empty( $res ) ? count( $res ) : 0;
}
// Check if requested index is a registered post type.
if ( in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
return aioseo()->sitemap->query->posts( aioseo()->sitemap->indexName, [ 'count' => true ] );
}
// Check if requested index is a registered taxonomy.
if ( in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedTaxonomies(), true ) ) {
return aioseo()->sitemap->query->terms( aioseo()->sitemap->indexName, [ 'count' => true ] );
}
return 0;
}
/**
* Checks if the requested sitemap is enabled.
*
* @since 4.0.0
*
* @return boolean Whether the sitemap is enabled.
*/
public function isEnabled() {
$options = aioseo()->options->noConflict();
if ( ! $options->sitemap->{aioseo()->sitemap->type}->enable ) {
return false;
}
if ( $options->sitemap->{aioseo()->sitemap->type}->postTypes->all ) {
return true;
}
$included = aioseo()->sitemap->helpers->includedPostTypes();
return ! empty( $included );
}
/**
* Returns all sitemap entries if indexing is disabled.
*
* @since 4.0.0
*
* @return array $entries The sitemap entries.
*/
private function nonIndexed() {
$additional = $this->addl();
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
$isStaticHomepage = 'page' === get_option( 'show_on_front' );
$blogPageEntry = [];
$homePageEntry = ! $isStaticHomepage ? [ array_shift( $additional ) ] : [];
$entries = array_merge( $additional, $this->author(), $this->date(), $this->postArchive() );
if ( $postTypes ) {
foreach ( $postTypes as $postType ) {
$postTypeEntries = $this->posts( $postType );
// If we don't have a static homepage, it's business as usual.
if ( ! $isStaticHomepage ) {
$entries = array_merge( $entries, $postTypeEntries );
continue;
}
$homePageId = (int) get_option( 'page_on_front' );
$blogPageId = (int) get_option( 'page_for_posts' );
if ( 'post' === $postType && $blogPageId ) {
$blogPageEntry[] = array_shift( $postTypeEntries );
}
if ( 'page' === $postType && $homePageId ) {
$homePageEntry[] = array_shift( $postTypeEntries );
}
$entries = array_merge( $entries, $postTypeEntries );
}
}
$taxonomies = aioseo()->sitemap->helpers->includedTaxonomies();
if ( $taxonomies ) {
foreach ( $taxonomies as $taxonomy ) {
$entries = array_merge( $entries, $this->terms( $taxonomy ) );
}
}
// Sort first by priority, then by last modified date.
usort( $entries, function ( $a, $b ) {
// If the priorities are equal, sort by last modified date.
if ( $a['priority'] === $b['priority'] ) {
return $a['lastmod'] > $b['lastmod'] ? -1 : 1;
}
return $a['priority'] > $b['priority'] ? -1 : 1;
} );
// Merge the arrays with the home page always first.
return array_merge( $homePageEntry, $blogPageEntry, $entries );
}
/**
* Returns all post entries for a given post type.
*
* @since 4.0.0
*
* @param string $postType The name of the post type.
* @param array $additionalArgs Any additional arguments for the post query.
* @return array The sitemap entries.
*/
public function posts( $postType, $additionalArgs = [] ) {
$posts = aioseo()->sitemap->query->posts( $postType, $additionalArgs );
if ( ! $posts ) {
return [];
}
// Return if we're determining the root indexes.
if ( ! empty( $additionalArgs['root'] ) && $additionalArgs['root'] ) {
return $posts;
}
$entries = [];
$isStaticHomepage = 'page' === get_option( 'show_on_front' );
$homePageId = (int) get_option( 'page_on_front' );
$excludeImages = aioseo()->sitemap->helpers->excludeImages();
foreach ( $posts as $post ) {
$entry = [
'loc' => get_permalink( $post->ID ),
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $this->getLastModified( $post ) ),
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', $post, $postType ),
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', $post, $postType ),
];
if ( ! $excludeImages ) {
$entry['images'] = ! empty( $post->images ) ? json_decode( $post->images ) : [];
}
// Override priority/frequency for static homepage.
if ( $isStaticHomepage && ( $homePageId === $post->ID || aioseo()->helpers->wpmlIsHomePage( $post->ID ) ) ) {
$entry['loc'] = aioseo()->helpers->maybeRemoveTrailingSlash( aioseo()->helpers->wpmlHomeUrl( $post->ID ) ?: $entry['loc'] );
$entry['changefreq'] = aioseo()->sitemap->priority->frequency( 'homePage' );
$entry['priority'] = aioseo()->sitemap->priority->priority( 'homePage' );
}
$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $post->ID, $postType, 'post' );
}
// We can't remove the post type here because other plugins rely on it.
return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
}
/**
* Returns all post archive entries.
*
* @since 4.0.0
*
* @return array $entries The sitemap entries.
*/
private function postArchive() {
$entries = [];
foreach ( aioseo()->sitemap->helpers->includedPostTypes( true ) as $postType ) {
if (
aioseo()->dynamicOptions->noConflict()->searchAppearance->archives->has( $postType ) &&
! aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->default &&
aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->noindex
) {
continue;
}
$post = aioseo()->core->db
->start( aioseo()->core->db->db->posts . ' as p', true )
->select( 'p.ID' )
->where( 'p.post_status', 'publish' )
->where( 'p.post_type', $postType )
->limit( 1 )
->run()
->result();
if ( ! $post ) {
continue;
}
$url = get_post_type_archive_link( $postType );
if ( $url ) {
$entry = [
'loc' => $url,
'lastmod' => aioseo()->sitemap->helpers->lastModifiedPostTime( $postType ),
'changefreq' => aioseo()->sitemap->priority->frequency( 'archive' ),
'priority' => aioseo()->sitemap->priority->priority( 'archive' ),
];
// To be consistent with our other entry filters, we need to pass the entry ID as well, but as null in this case.
$entries[] = apply_filters( 'aioseo_sitemap_archive_entry', $entry, null, $postType, 'archive' );
}
}
return apply_filters( 'aioseo_sitemap_post_archives', $entries );
}
/**
* Returns all term entries for a given taxonomy.
*
* @since 4.0.0
*
* @param string $taxonomy The name of the taxonomy.
* @param array $additionalArgs Any additional arguments for the term query.
* @return array The sitemap entries.
*/
public function terms( $taxonomy, $additionalArgs = [] ) {
$terms = aioseo()->sitemap->query->terms( $taxonomy, $additionalArgs );
if ( ! $terms ) {
return [];
}
// Get all registered post types for the taxonomy.
$postTypes = [];
foreach ( get_post_types() as $postType ) {
$taxonomies = get_object_taxonomies( $postType );
foreach ( $taxonomies as $name ) {
if ( $taxonomy === $name ) {
$postTypes[] = $postType;
}
}
}
// Return if we're determining the root indexes.
if ( ! empty( $additionalArgs['root'] ) && $additionalArgs['root'] ) {
return $terms;
}
$entries = [];
foreach ( $terms as $term ) {
$entry = [
'loc' => get_term_link( $term->term_id ),
'lastmod' => $this->getTermLastModified( $term ),
'changefreq' => aioseo()->sitemap->priority->frequency( 'taxonomies', $term, $taxonomy ),
'priority' => aioseo()->sitemap->priority->priority( 'taxonomies', $term, $taxonomy ),
'images' => aioseo()->sitemap->image->term( $term )
];
$entries[] = apply_filters( 'aioseo_sitemap_term', $entry, $term->term_id, $term->taxonomy, 'term' );
}
return apply_filters( 'aioseo_sitemap_terms', $entries );
}
/**
* Returns the last modified date for a given term.
*
* @since 4.0.0
*
* @param int|object $term The term data object.
* @return string The lastmod timestamp.
*/
public function getTermLastModified( $term ) {
$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
$termTaxonomyTable = aioseo()->core->db->db->prefix . 'term_taxonomy';
// If the term is an ID, get the term object.
if ( is_numeric( $term ) ) {
$term = aioseo()->helpers->getTerm( $term );
}
// First, check the count of the term. If it's 0, then we're dealing with a parent term that does not have
// posts assigned to it. In this case, we need to get the last modified date of all its children.
if ( empty( $term->count ) ) {
$lastModified = aioseo()->core->db
->start( aioseo()->core->db->db->posts . ' as p', true )
->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
->where( 'p.post_status', 'publish' )
->whereRaw( "
( `p`.`ID` IN
(
SELECT CONVERT(`tr`.`object_id`, unsigned)
FROM `$termRelationshipsTable` as tr
JOIN `$termTaxonomyTable` as tt ON `tr`.`term_taxonomy_id` = `tt`.`term_taxonomy_id`
WHERE `tt`.`term_id` IN
(
SELECT `tt`.`term_id`
FROM `$termTaxonomyTable` as tt
WHERE `tt`.`parent` = '{$term->term_id}'
)
)
)" )
->run()
->result();
} else {
$lastModified = aioseo()->core->db
->start( aioseo()->core->db->db->posts . ' as p', true )
->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
->where( 'p.post_status', 'publish' )
->whereRaw( "
( `p`.`ID` IN
(
SELECT CONVERT(`tr`.`object_id`, unsigned)
FROM `$termRelationshipsTable` as tr
JOIN `$termTaxonomyTable` as tt ON `tr`.`term_taxonomy_id` = `tt`.`term_taxonomy_id`
WHERE `tt`.`term_id` = '{$term->term_id}'
)
)" )
->run()
->result();
}
$lastModified = $lastModified[0]->last_modified ?? '';
return aioseo()->helpers->dateTimeToIso8601( $lastModified );
}
/**
* Returns all additional pages.
*
* @since 4.0.0
*
* @param bool $shouldChunk Whether the entries should be chuncked. Is set to false when the static sitemap is generated.
* @return array The sitemap entries.
*/
public function addl( $shouldChunk = true ) {
$additionalPages = [];
if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
$additionalPages = array_map( 'json_decode', aioseo()->options->sitemap->general->additionalPages->pages );
$additionalPages = array_filter( $additionalPages, function( $additionalPage ) {
return ! empty( $additionalPage->url );
} );
}
$entries = [];
foreach ( $additionalPages as $additionalPage ) {
$entries[] = [
'loc' => $additionalPage->url,
'lastmod' => aioseo()->sitemap->helpers->lastModifiedAdditionalPage( $additionalPage ),
'changefreq' => $additionalPage->frequency->value,
'priority' => $additionalPage->priority->value,
'isTimezone' => true
];
}
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
$shouldIncludeHomepage = 'posts' === get_option( 'show_on_front' ) || ! in_array( 'page', $postTypes, true );
if ( $shouldIncludeHomepage ) {
$frontPageId = (int) get_option( 'page_on_front' );
$frontPageUrl = aioseo()->helpers->localizedUrl( '/' );
$post = aioseo()->helpers->getPost( $frontPageId );
$homepageEntry = [
'loc' => aioseo()->helpers->maybeRemoveTrailingSlash( $frontPageUrl ),
'lastmod' => $post ? aioseo()->helpers->dateTimeToIso8601( $this->getLastModified( $post ) ) : aioseo()->sitemap->helpers->lastModifiedPostTime(),
'changefreq' => aioseo()->sitemap->priority->frequency( 'homePage' ),
'priority' => aioseo()->sitemap->priority->priority( 'homePage' )
];
$translatedHomepages = aioseo()->helpers->wpmlHomePages();
foreach ( $translatedHomepages as $languageCode => $translatedHomepage ) {
if ( untrailingslashit( $translatedHomepage['url'] ) !== untrailingslashit( $homepageEntry['loc'] ) ) {
$homepageEntry['languages'][] = [
'language' => $languageCode,
'location' => $translatedHomepage['url']
];
}
}
// Add homepage to the first position.
array_unshift( $entries, $homepageEntry );
}
if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
$entries = apply_filters( 'aioseo_sitemap_additional_pages', $entries );
}
if ( empty( $entries ) ) {
return [];
}
if ( aioseo()->options->sitemap->general->indexes && $shouldChunk ) {
$entries = aioseo()->sitemap->helpers->chunkEntries( $entries );
$entries = $entries[ aioseo()->sitemap->pageNumber ] ?? [];
}
return $entries;
}
/**
* Returns all author archive entries.
*
* @since 4.0.0
*
* @return array The sitemap entries.
*/
public function author() {
if (
! aioseo()->sitemap->helpers->lastModifiedPost() ||
! aioseo()->options->sitemap->general->author ||
! aioseo()->options->searchAppearance->archives->author->show ||
(
! aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default &&
aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex
) ||
(
aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default &&
(
! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default &&
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
)
)
) {
return [];
}
// Allow users to filter the authors in case their sites use a membership plugin or have custom code that affect the authors on their site.
// e.g. there might be additional roles/conditions that need to be checked here.
$authors = apply_filters( 'aioseo_sitemap_authors', [] );
if ( empty( $authors ) ) {
$usersTableName = aioseo()->core->db->db->users; // We get the table name from WPDB since multisites share the same table.
$authors = aioseo()->core->db->start( "$usersTableName as u", true )
->select( 'u.ID as ID, u.user_nicename as nicename, MAX(p.post_modified_gmt) as lastModified' )
->join( 'posts as p', 'u.ID = p.post_author' )
->where( 'p.post_status', 'publish' )
->whereIn( 'p.post_type', aioseo()->sitemap->helpers->getAuthorPostTypes() )
->groupBy( 'u.ID' )
->orderBy( 'lastModified DESC' )
->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->pageNumber * aioseo()->sitemap->linksPerIndex )
->run()
->result();
}
if ( empty( $authors ) ) {
return [];
}
$entries = [];
foreach ( $authors as $authorData ) {
$entry = [
'loc' => ! empty( $authorData->authorUrl )
? $authorData->authorUrl
: get_author_posts_url( $authorData->ID, $authorData->nicename ?: '' ),
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $authorData->lastModified ),
'changefreq' => aioseo()->sitemap->priority->frequency( 'author' ),
'priority' => aioseo()->sitemap->priority->priority( 'author' )
];
$entries[] = apply_filters( 'aioseo_sitemap_author_entry', $entry, $authorData->ID, $authorData->nicename, 'author' );
}
return apply_filters( 'aioseo_sitemap_author_archives', $entries );
}
/**
* Returns all data archive entries.
*
* @since 4.0.0
*
* @return array The sitemap entries.
*/
public function date() {
if (
! aioseo()->sitemap->helpers->lastModifiedPost() ||
! aioseo()->options->sitemap->general->date ||
! aioseo()->options->searchAppearance->archives->date->show ||
(
! aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default &&
aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex
) ||
(
aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default &&
(
! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default &&
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
)
)
) {
return [];
}
$postsTable = aioseo()->core->db->db->posts;
$dates = aioseo()->core->db->execute(
"SELECT
YEAR(post_date) AS `year`,
MONTH(post_date) AS `month`,
post_date_gmt,
post_modified_gmt
FROM {$postsTable}
WHERE post_type = 'post' AND post_status = 'publish'
GROUP BY
YEAR(post_date),
MONTH(post_date)
ORDER BY post_date ASC
LIMIT 50000",
true
)->result();
if ( empty( $dates ) ) {
return [];
}
$entries = [];
$year = '';
foreach ( $dates as $date ) {
$entry = [
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $this->getLastModified( $date ) ),
'changefreq' => aioseo()->sitemap->priority->frequency( 'date' ),
'priority' => aioseo()->sitemap->priority->priority( 'date' ),
];
// Include each year only once.
if ( $year !== $date->year ) {
$year = $date->year;
$entry['loc'] = get_year_link( $date->year );
$entries[] = apply_filters( 'aioseo_sitemap_date_entry', $entry, $date, 'year', 'date' );
}
$entry['loc'] = get_month_link( $date->year, $date->month );
$entries[] = apply_filters( 'aioseo_sitemap_date_entry', $entry, $date, 'month', 'date' );
}
return apply_filters( 'aioseo_sitemap_date_archives', $entries );
}
/**
* Returns all entries for the RSS Sitemap.
*
* @since 4.0.0
*
* @return array The sitemap entries.
*/
public function rss() {
$posts = aioseo()->sitemap->query->posts(
aioseo()->sitemap->helpers->includedPostTypes(),
[ 'orderBy' => '`p`.`post_modified_gmt` DESC' ]
);
if ( ! count( $posts ) ) {
return [];
}
$entries = [];
foreach ( $posts as $post ) {
$entry = [
'guid' => get_permalink( $post->ID ),
'title' => get_the_title( $post ),
'description' => get_post_field( 'post_excerpt', $post->ID ),
'pubDate' => aioseo()->helpers->dateTimeToRfc822( $this->getLastModified( $post ) )
];
// If the entry is the homepage, we need to check if the permalink structure
// does not have a trailing slash. If so, we need to strip it because WordPress adds it
// regardless for the home_url() in get_page_link() which is used in the get_permalink() function.
static $homeId = null;
if ( null === $homeId ) {
$homeId = get_option( 'page_for_posts' );
}
if ( aioseo()->helpers->getHomePageId() === $post->ID ) {
$entry['guid'] = aioseo()->helpers->maybeRemoveTrailingSlash( $entry['guid'] );
}
$entries[] = apply_filters( 'aioseo_sitemap_post_rss', $entry, $post->ID, $post->post_type, 'post' );
}
usort( $entries, function( $a, $b ) {
return $a['pubDate'] < $b['pubDate'] ? 1 : 0;
});
return apply_filters( 'aioseo_sitemap_rss', $entries );
}
/**
* Returns the last modified date for a given post.
*
* @since 4.6.3
*
* @param object $post The post object.
*
* @return string The last modified date.
*/
public function getLastModified( $post ) {
$publishDate = $post->post_date_gmt;
$lastModifiedDate = $post->post_modified_gmt;
// Get the date which is the latest.
return $lastModifiedDate > $publishDate ? $lastModifiedDate : $publishDate;
}
/**
* Returns all entries for the BuddyPress Activity Sitemap.
* This method is automagically called from {@see get()} if the current index name equals to 'bp-activity'
*
* @since 4.7.6
*
* @return array The sitemap entries.
*/
public function bpActivity() {
$entries = [];
if ( ! in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
return $entries;
}
$postType = 'bp-activity';
$query = aioseo()->core->db
->start( 'bp_activity as a' )
->select( '`a`.`id`, `a`.`date_recorded`' )
->whereRaw( "a.is_spam = 0 AND a.hide_sitewide = 0 AND a.type NOT IN ('activity_comment', 'last_activity')" )
->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset )
->orderBy( 'a.date_recorded DESC' );
$items = $query->run()
->result();
foreach ( $items as $item ) {
$entry = [
'loc' => BuddyPressIntegration::getComponentSingleUrl( 'activity', $item->id ),
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $item->date_recorded ),
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
];
$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $item->id, $postType );
}
$archiveUrl = BuddyPressIntegration::getComponentArchiveUrl( 'activity' );
if (
aioseo()->helpers->isUrl( $archiveUrl ) &&
! in_array( $postType, aioseo()->helpers->getNoindexedObjects( 'archives' ), true )
) {
$lastMod = ! empty( $items[0] ) ? $items[0]->date_recorded : current_time( 'mysql' );
$entry = [
'loc' => $archiveUrl,
'lastmod' => $lastMod,
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
];
array_unshift( $entries, $entry );
}
return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
}
/**
* Returns all entries for the BuddyPress Group Sitemap.
* This method is automagically called from {@see get()} if the current index name equals to 'bp-group'
*
* @since 4.7.6
*
* @return array The sitemap entries.
*/
public function bpGroup() {
$entries = [];
if ( ! in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
return $entries;
}
$postType = 'bp-group';
$query = aioseo()->core->db
->start( 'bp_groups as g' )
->select( '`g`.`id`, `g`.`date_created`, `gm`.`meta_value` as date_modified' )
->leftJoin( 'bp_groups_groupmeta as gm', 'g.id = gm.group_id' )
->whereRaw( "g.status = 'public' AND gm.meta_key = 'last_activity'" )
->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset )
->orderBy( 'gm.meta_value DESC' )
->orderBy( 'g.date_created DESC' );
$items = $query->run()
->result();
foreach ( $items as $item ) {
$lastMod = $item->date_modified ?: $item->date_created;
$entry = [
'loc' => BuddyPressIntegration::getComponentSingleUrl( 'group', BuddyPressIntegration::callFunc( 'bp_get_group_by', 'id', $item->id ) ),
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $lastMod ),
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
];
$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $item->id, $postType );
}
$archiveUrl = BuddyPressIntegration::getComponentArchiveUrl( 'group' );
if (
aioseo()->helpers->isUrl( $archiveUrl ) &&
! in_array( $postType, aioseo()->helpers->getNoindexedObjects( 'archives' ), true )
) {
$lastMod = ! empty( $items[0] ) ? $items[0]->date_modified : current_time( 'mysql' );
$entry = [
'loc' => $archiveUrl,
'lastmod' => $lastMod,
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
];
array_unshift( $entries, $entry );
}
return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
}
/**
* Returns all entries for the BuddyPress Member Sitemap.
* This method is automagically called from {@see get()} if the current index name equals to 'bp-member'
*
* @since 4.7.6
*
* @return array The sitemap entries.
*/
public function bpMember() {
$entries = [];
if ( ! in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
return $entries;
}
$postType = 'bp-member';
$query = aioseo()->core->db
->start( 'bp_activity as a' )
->select( '`a`.`user_id` as id, `a`.`date_recorded`' )
->whereRaw( "a.component = 'members' AND a.type = 'last_activity'" )
->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset )
->orderBy( 'a.date_recorded DESC' );
$items = $query->run()
->result();
foreach ( $items as $item ) {
$entry = [
'loc' => BuddyPressIntegration::getComponentSingleUrl( 'member', $item->id ),
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $item->date_recorded ),
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
];
$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $item->id, $postType );
}
$archiveUrl = BuddyPressIntegration::getComponentArchiveUrl( 'member' );
if (
aioseo()->helpers->isUrl( $archiveUrl ) &&
! in_array( $postType, aioseo()->helpers->getNoindexedObjects( 'archives' ), true )
) {
$lastMod = ! empty( $items[0] ) ? $items[0]->date_recorded : current_time( 'mysql' );
$entry = [
'loc' => $archiveUrl,
'lastmod' => $lastMod,
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
];
array_unshift( $entries, $entry );
}
return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
}
/**
* Returns all entries for the WooCommerce Product Attributes sitemap.
* Note: This sitemap does not support pagination.
*
* @since 4.7.8
*
* @param bool $count Whether to return the count of the entries. This is used to determine the indexes.
* @return array The sitemap entries.
*/
public function productAttributes( $count = false ) {
$aioseoTermsTable = aioseo()->core->db->prefix . 'aioseo_terms';
$wcAttributeTaxonomiesTable = aioseo()->core->db->prefix . 'woocommerce_attribute_taxonomies';
$termTaxonomyTable = aioseo()->core->db->prefix . 'term_taxonomy';
$selectClause = 'COUNT(*) as childProductAttributes';
if ( ! $count ) {
$selectClause = aioseo()->pro ? 'tt.term_id, tt.taxonomy, at.frequency, at.priority' : 'tt.term_id, tt.taxonomy';
}
$joinClause = aioseo()->pro ? "LEFT JOIN {$aioseoTermsTable} AS at ON tt.term_id = at.term_id" : '';
$whereClause = aioseo()->pro ? 'AND (at.robots_noindex IS NULL OR at.robots_noindex = 0)' : '';
$limitClause = $count ? '' : 'LIMIT 50000';
$result = aioseo()->core->db->execute(
"SELECT {$selectClause}
FROM {$termTaxonomyTable} AS tt
JOIN {$wcAttributeTaxonomiesTable} AS wat ON tt.taxonomy = CONCAT('pa_', wat.attribute_name)
{$joinClause}
WHERE wat.attribute_public = 1
{$whereClause}
AND tt.count > 0
{$limitClause};",
true
)->result();
if ( $count ) {
return ! empty( $result[0]->childProductAttributes ) ? (int) $result[0]->childProductAttributes : 0;
}
if ( empty( $result ) ) {
return [];
}
$entries = [];
foreach ( $result as $term ) {
$term = (object) $term;
$termId = (int) $term->term_id;
$entry = [
'loc' => get_term_link( $termId ),
'lastmod' => $this->getTermLastModified( $termId ),
'changefreq' => aioseo()->sitemap->priority->frequency( 'taxonomies', $term, 'product_attributes' ),
'priority' => aioseo()->sitemap->priority->priority( 'taxonomies', $term, 'product_attributes' ),
'images' => aioseo()->sitemap->image->term( $term )
];
$entries[] = apply_filters( 'aioseo_sitemap_product_attributes', $entry, $termId, $term->taxonomy, 'term' );
}
return $entries;
}
} Sitemap/File.php 0000666 00000020235 15113050717 0007544 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the static sitemap.
*
* @since 4.0.0
*/
class File {
/**
* Whether the static files have already been updated during the current request.
*
* We keep track of this so that setting changes to do not trigger the regeneration multiple times.
*
* @since 4.0.0
*
* @var boolean
*/
private static $isUpdated = false;
/**
* Generates the static sitemap files.
*
* @since 4.0.0
*
* @param boolean $force Whether or not to force it through.
* @return void
*/
public function generate( $force = false ) {
aioseo()->addons->doAddonFunction( 'file', 'generate', [ $force ] );
// Exit if static sitemap generation isn't enabled.
if (
! $force &&
(
self::$isUpdated ||
! aioseo()->options->sitemap->general->enable ||
! aioseo()->options->sitemap->general->advancedSettings->enable ||
! in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) ||
aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic
)
) {
return;
}
$files = [];
self::$isUpdated = true;
// We need to set these values here as setContext() doesn't run.
// Subsequently, we need to manually reset the index name below for each query we run.
// Also, since we need to chunck the entries manually, we cannot limit any queries and need to reset the amount of allowed URLs per index.
aioseo()->sitemap->offset = 0;
aioseo()->sitemap->type = 'general';
$sitemapName = aioseo()->sitemap->helpers->filename();
aioseo()->sitemap->indexes = aioseo()->options->sitemap->general->indexes;
aioseo()->sitemap->linksPerIndex = PHP_INT_MAX;
aioseo()->sitemap->isStatic = true;
$additionalPages = [];
if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
foreach ( aioseo()->options->sitemap->general->additionalPages->pages as $additionalPage ) {
$additionalPage = json_decode( $additionalPage );
if ( empty( $additionalPage->url ) ) {
continue;
}
// Decode Additional Page Url to properly show Unicode Characters.
$additionalPages[] = $additionalPage;
}
}
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
$additionalPages = apply_filters( 'aioseo_sitemap_additional_pages', $additionalPages );
if (
'posts' === get_option( 'show_on_front' ) ||
count( $additionalPages ) ||
! in_array( 'page', $postTypes, true )
) {
$entries = aioseo()->sitemap->content->addl( false );
$filename = "addl-$sitemapName.xml";
$files[ $filename ] = [
'total' => count( $entries ),
'entries' => $entries
];
}
if (
aioseo()->sitemap->helpers->lastModifiedPost() &&
aioseo()->options->sitemap->general->author
) {
$entries = aioseo()->sitemap->content->author();
$filename = "author-$sitemapName.xml";
$files[ $filename ] = [
'total' => count( $entries ),
'entries' => $entries
];
}
if (
aioseo()->sitemap->helpers->lastModifiedPost() &&
aioseo()->options->sitemap->general->date
) {
$entries = aioseo()->sitemap->content->date();
$filename = "date-$sitemapName.xml";
$files[ $filename ] = [
'total' => count( $entries ),
'entries' => $entries
];
}
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
if ( $postTypes ) {
foreach ( $postTypes as $postType ) {
aioseo()->sitemap->indexName = $postType;
$posts = aioseo()->sitemap->content->posts( $postType );
if ( ! $posts ) {
continue;
}
$total = aioseo()->sitemap->query->posts( $postType, [ 'count' => true ] );
// We need to temporarily reset the linksPerIndex count here so that we can properly chunk.
aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
$chunks = aioseo()->sitemap->helpers->chunkEntries( $posts );
aioseo()->sitemap->linksPerIndex = PHP_INT_MAX;
if ( 1 === count( $chunks ) ) {
$filename = "$postType-$sitemapName.xml";
$files[ $filename ] = [
'total' => $total,
'entries' => $chunks[0]
];
} else {
for ( $i = 1; $i <= count( $chunks ); $i++ ) {
$filename = "$postType-$sitemapName$i.xml";
$files[ $filename ] = [
'total' => $total,
'entries' => $chunks[ $i - 1 ]
];
}
}
}
}
$taxonomies = aioseo()->sitemap->helpers->includedTaxonomies();
if ( $taxonomies ) {
foreach ( $taxonomies as $taxonomy ) {
aioseo()->sitemap->indexName = $taxonomy;
$terms = aioseo()->sitemap->content->terms( $taxonomy );
if ( ! $terms ) {
continue;
}
$total = aioseo()->sitemap->query->terms( $taxonomy, [ 'count' => true ] );
// We need to temporarily reset the linksPerIndex count here so that we can properly chunk.
aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
$chunks = aioseo()->sitemap->helpers->chunkEntries( $terms );
aioseo()->sitemap->linksPerIndex = PHP_INT_MAX;
if ( 1 === count( $chunks ) ) {
$filename = "$taxonomy-$sitemapName.xml";
$files[ $filename ] = [
'total' => $total,
'entries' => $chunks[0]
];
} else {
for ( $i = 1; $i <= count( $chunks ); $i++ ) {
$filename = "$taxonomy-$sitemapName$i.xml";
$files[ $filename ] = [
'total' => $total,
'entries' => $chunks[ $i - 1 ]
];
}
}
}
}
$this->writeSitemaps( $files );
}
/**
* Writes all sitemap files.
*
* @since 4.0.0
*
* @param array $files The sitemap files.
* @return void
*/
public function writeSitemaps( $files ) {
$sitemapName = aioseo()->sitemap->helpers->filename();
if ( aioseo()->sitemap->indexes ) {
$indexes = [];
foreach ( $files as $filename => $data ) {
if ( empty( $data['entries'] ) ) {
continue;
}
$indexes[] = [
'loc' => trailingslashit( home_url() ) . $filename,
'lastmod' => array_values( $data['entries'] )[0]['lastmod'],
'count' => count( $data['entries'] )
];
}
$files[ "$sitemapName.xml" ] = [
'total' => 0,
'entries' => $indexes,
];
foreach ( $files as $filename => $data ) {
$this->writeSitemap( $filename, $data['entries'], $data['total'] );
}
return;
}
$content = [];
foreach ( $files as $filename => $data ) {
foreach ( $data['entries'] as $entry ) {
$content[] = $entry;
}
}
$this->writeSitemap( "$sitemapName.xml", $content, count( $content ) );
}
/**
* Writes a given sitemap file to the root dir.
*
* Helper function for writeSitemaps().
*
* @since 4.0.0
*
* @param string $filename The name of the file.
* @param array $entries The sitemap entries for the file.
* @return void
*/
protected function writeSitemap( $filename, $entries, $total = 0 ) {
$sitemapName = aioseo()->sitemap->helpers->filename();
aioseo()->sitemap->indexName = $filename;
if ( "$sitemapName.xml" === $filename && aioseo()->sitemap->indexes ) {
// Set index name to root so that we use the right output template.
aioseo()->sitemap->indexName = 'root';
}
aioseo()->sitemap->xsl->saveXslData( $filename, $entries, $total );
ob_start();
aioseo()->sitemap->output->output( $entries );
aioseo()->addons->doAddonFunction( 'output', 'output', [ $entries, $total ] );
$content = ob_get_clean();
$fs = aioseo()->core->fs;
$file = ABSPATH . sanitize_file_name( $filename );
$fileExists = $fs->exists( $file );
if ( ! $fileExists || $fs->isWritable( $file ) ) {
$fs->putContents( $file, $content );
}
}
/**
* Return an array of sitemap files.
*
* @since 4.0.0
*
* @return array An array of files.
*/
public function files() {
require_once ABSPATH . 'wp-admin/includes/file.php';
$files = list_files( get_home_path(), 1 );
if ( ! count( $files ) ) {
return [];
}
$sitemapFiles = [];
foreach ( $files as $filename ) {
if ( preg_match( '#.*sitemap.*#', (string) $filename ) ) {
$sitemapFiles[] = $filename;
}
}
return $sitemapFiles;
}
} Sitemap/SitemapAbstract.php 0000666 00000004054 15113050717 0011754 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Abstract class holding the class properties of our main AIOSEO class.
*
* @since 4.4.3
*/
abstract class SitemapAbstract {
/**
* Content class instance.
*
* @since 4.2.7
*
* @var Content
*/
public $content = null;
/**
* Root class instance.
*
* @since 4.2.7
*
* @var Root
*/
public $root = null;
/**
* Query class instance.
*
* @since 4.2.7
*
* @var Query
*/
public $query = null;
/**
* File class instance.
*
* @since 4.2.7
*
* @var File
*/
public $file = null;
/**
* Image class instance.
*
* @since 4.2.7
*
* @var Image\Image
*/
public $image = null;
/**
* Priority class instance.
*
* @since 4.2.7
*
* @var Priority
*/
public $priority = null;
/**
* Output class instance.
*
* @since 4.2.7
*
* @var Output
*/
public $output = null;
/**
* Helpers class instance.
*
* @since 4.2.7
*
* @var Helpers
*/
public $helpers = null;
/**
* RequestParser class instance.
*
* @since 4.2.7
*
* @var RequestParser
*/
public $requestParser = null;
/**
* Xsl class instance.
*
* @since 4.2.7
*
* @var Xsl
*/
public $xsl = null;
/**
* The sitemap type (e.g. "general", "news", "video", "rss", etc.).
*
* @since 4.2.7
*
* @var string
*/
public $type = '';
/**
* Index name.
*
* @since 4.4.3
*
* @var string
*/
public $indexName = '';
/**
* Page number.
*
* @since 4.4.3
*
* @var int
*/
public $pageNumber = 0;
/**
* Page number.
*
* @since 4.4.3
*
* @var int
*/
public $offset = 0;
/**
* Indexes active.
*
* @since 4.4.3
*
* @var bool
*/
public $indexes = false;
/**
* Links per index.
*
* @since 4.4.3
*
* @var int
*/
public $linksPerIndex = PHP_INT_MAX;
/**
* Is static.
*
* @since 4.4.3
*
* @var bool
*/
public $isStatic = false;
/**
* Filename.
*
* @since 4.4.3
*
* @var string
*/
public $filename = '';
} Sitemap/Helpers.php 0000666 00000043214 15113050717 0010271 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Contains general helper methods specific to the sitemap.
*
* @since 4.0.0
*/
class Helpers {
/**
* Used to track the performance of the sitemap.
*
* @since 4.0.0
*
* @var array
* $memory The peak memory that is required to generate the sitemap.
* $time The time that is required to generate the sitemap.
*/
private $performance;
/**
* Returns the sitemap filename.
*
* @since 4.0.0
*
* @param string $type The sitemap type. We pass it in when we need to get the filename for a specific sitemap outside of the context of the sitemap.
* @return string The sitemap filename.
*/
public function filename( $type = '' ) {
if ( ! $type ) {
$type = isset( aioseo()->sitemap->type ) ? aioseo()->sitemap->type : 'general';
}
return apply_filters( 'aioseo_sitemap_filename', aioseo()->options->sitemap->$type->filename );
}
/**
* Returns the last modified post.
*
* @since 4.0.0
*
* @param array $additionalArgs Any additional arguments for the post query.
* @return mixed WP_Post object or false.
*/
public function lastModifiedPost( $additionalArgs = [] ) {
$args = [
'post_status' => 'publish',
'posts_per_page' => 1,
'orderby ' => 'modified',
'order' => 'ASC'
];
if ( $additionalArgs ) {
foreach ( $additionalArgs as $k => $v ) {
$args[ $k ] = $v;
}
}
$query = ( new \WP_Query( $args ) );
if ( ! $query->post_count ) {
return false;
}
return $query->posts[0];
}
/**
* Returns the timestamp of the last modified post.
*
* @since 4.0.0
*
* @param array $postTypes The relevant post types.
* @param array $additionalArgs Any additional arguments for the post query.
* @return string Formatted date string (ISO 8601).
*/
public function lastModifiedPostTime( $postTypes = [ 'post', 'page' ], $additionalArgs = [] ) {
if ( is_array( $postTypes ) ) {
$postTypes = implode( "', '", $postTypes );
}
$query = aioseo()->core->db
->start( aioseo()->core->db->db->posts . ' as p', true )
->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
->where( 'p.post_status', 'publish' )
->whereRaw( "( `p`.`post_type` IN ( '$postTypes' ) )" );
if ( isset( $additionalArgs['author'] ) ) {
$query->where( 'p.post_author', $additionalArgs['author'] );
}
$lastModified = $query->run()
->result();
return ! empty( $lastModified[0]->last_modified )
? aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->last_modified )
: '';
}
/**
* Returns the timestamp of the last modified additional page.
*
* @since 4.0.0
*
* @return string Formatted date string (ISO 8601).
*/
public function lastModifiedAdditionalPagesTime() {
$pages = [];
if ( 'posts' === get_option( 'show_on_front' ) || ! in_array( 'page', $this->includedPostTypes(), true ) ) {
$frontPageId = (int) get_option( 'page_on_front' );
$post = aioseo()->helpers->getPost( $frontPageId );
$pages[] = $post ? strtotime( $post->post_modified_gmt ) : strtotime( aioseo()->sitemap->helpers->lastModifiedPostTime() );
}
foreach ( aioseo()->options->sitemap->general->additionalPages->pages as $page ) {
$additionalPage = json_decode( $page );
if ( empty( $additionalPage->url ) ) {
continue;
}
$pages[] = strtotime( $additionalPage->lastModified );
}
if ( empty( $pages ) ) {
$additionalPages = apply_filters( 'aioseo_sitemap_additional_pages', [] );
if ( empty( $additionalPages ) ) {
return false;
}
$lastModified = 0;
$timestamp = time();
foreach ( $additionalPages as $page ) {
if ( empty( $page['lastmod'] ) ) {
continue;
}
$timestamp = strtotime( $page['lastmod'] );
if ( ! $timestamp ) {
continue;
}
if ( $lastModified < $timestamp ) {
$lastModified = $timestamp;
}
}
return 0 !== $lastModified ? aioseo()->helpers->dateTimeToIso8601( gmdate( 'Y-m-d H:i:s', $timestamp ) ) : false;
}
return aioseo()->helpers->dateTimeToIso8601( gmdate( 'Y-m-d H:i:s', max( $pages ) ) );
}
/**
* Formats a given image URL for usage in the sitemap.
*
* @since 4.0.0
*
* @param string $url The URL.
* @return string The formatted URL.
*/
public function formatUrl( $url ) {
// Remove URL parameters.
$url = strtok( $url, '?' );
$url = htmlspecialchars( $url, ENT_COMPAT, 'UTF-8', false );
return aioseo()->helpers->makeUrlAbsolute( $url );
}
/**
* Logs the performance of the sitemap for debugging purposes.
*
* @since 4.0.0
*
* @return void
*/
public function logPerformance() {
// Start logging the performance.
if ( ! $this->performance ) {
$this->performance['time'] = microtime( true );
$this->performance['memory'] = ( memory_get_peak_usage( true ) / 1024 ) / 1024;
return;
}
// Stop logging the performance.
$time = microtime( true ) - $this->performance['time'];
$memory = $this->performance['memory'];
$type = aioseo()->sitemap->type;
$indexName = aioseo()->sitemap->indexName;
// phpcs:disable WordPress.PHP.DevelopmentFunctions
error_log( wp_json_encode( "$indexName index of $type sitemap generated in $time seconds using a maximum of $memory mb of memory." ) );
// phpcs:enable WordPress.PHP.DevelopmentFunctions
}
/**
* Returns the post types that should be included in the sitemap.
*
* @since 4.0.0
*
* @param boolean $hasArchivesOnly Whether or not to only include post types which have archives.
* @return array $postTypes The included post types.
*/
public function includedPostTypes( $hasArchivesOnly = false ) {
if ( aioseo()->options->sitemap->{aioseo()->sitemap->type}->postTypes->all ) {
$postTypes = aioseo()->helpers->getPublicPostTypes( true, $hasArchivesOnly );
} else {
$postTypes = aioseo()->options->sitemap->{aioseo()->sitemap->type}->postTypes->included;
}
if ( ! $postTypes ) {
return $postTypes;
}
$options = aioseo()->options->noConflict();
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
$publicPostTypes = aioseo()->helpers->getPublicPostTypes( true, $hasArchivesOnly );
foreach ( $postTypes as $postType ) {
// Check if post type is no longer registered.
if ( ! in_array( $postType, $publicPostTypes, true ) || ! $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
continue;
}
// Check if post type isn't noindexed.
if ( aioseo()->helpers->isPostTypeNoindexed( $postType ) ) {
if ( ! $this->checkForIndexedPost( $postType ) ) {
$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
continue;
}
}
if (
$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default &&
! $options->searchAppearance->advanced->globalRobotsMeta->default &&
$options->searchAppearance->advanced->globalRobotsMeta->noindex
) {
if ( ! $this->checkForIndexedPost( $postType ) ) {
$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
}
}
}
return $postTypes;
}
/**
* Checks if any post is explicitly indexed when the post type is noindexed.
*
* @since 4.0.0
*
* @param string $postType The post type to check for.
* @return bool Whether or not there is an indexed post.
*/
private function checkForIndexedPost( $postType ) {
$db = aioseo()->core->db->noConflict();
$posts = $db->start( aioseo()->core->db->db->posts . ' as p', true )
->select( 'p.ID' )
->join( 'aioseo_posts as ap', '`ap`.`post_id` = `p`.`ID`' )
->where( 'p.post_status', 'attachment' === $postType ? 'inherit' : 'publish' )
->where( 'p.post_type', $postType )
->whereRaw( '( `ap`.`robots_default` = 0 AND `ap`.`robots_noindex` = 0 )' )
->limit( 1 )
->run()
->result();
if ( $posts && count( $posts ) ) {
return true;
}
return false;
}
/**
* Returns the taxonomies that should be included in the sitemap.
*
* @since 4.0.0
*
* @return array The included taxonomies.
*/
public function includedTaxonomies() {
$taxonomies = [];
if ( aioseo()->options->sitemap->{aioseo()->sitemap->type}->taxonomies->all ) {
$taxonomies = get_taxonomies();
} else {
$taxonomies = aioseo()->options->sitemap->{aioseo()->sitemap->type}->taxonomies->included;
}
if ( ! $taxonomies ) {
return [];
}
$options = aioseo()->options->noConflict();
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
$publicTaxonomies = aioseo()->helpers->getPublicTaxonomies( true );
foreach ( $taxonomies as $taxonomy ) {
if (
aioseo()->helpers->isWooCommerceActive() &&
aioseo()->helpers->isWooCommerceProductAttribute( $taxonomy )
) {
$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
if ( ! in_array( 'product_attributes', $taxonomies, true ) ) {
$taxonomies[] = 'product_attributes';
}
continue;
}
// Check if taxonomy is no longer registered.
if ( ! in_array( $taxonomy, $publicTaxonomies, true ) || ! $dynamicOptions->searchAppearance->taxonomies->has( $taxonomy ) ) {
$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
continue;
}
// Check if taxonomy isn't noindexed.
if ( aioseo()->helpers->isTaxonomyNoindexed( $taxonomy ) ) {
$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
continue;
}
if (
$dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->default &&
! $options->searchAppearance->advanced->globalRobotsMeta->default &&
$options->searchAppearance->advanced->globalRobotsMeta->noindex
) {
$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
continue;
}
}
return $taxonomies;
}
/**
* Splits sitemap entries into chuncks based on the max. amount of URLs per index.
*
* @since 4.0.0
*
* @param array $entries The sitemap entries.
* @return array The chunked sitemap entries.
*/
public function chunkEntries( $entries ) {
return array_chunk( $entries, aioseo()->sitemap->linksPerIndex, true );
}
/**
* Formats the last Modified date of a user-submitted additional page as an ISO 8601 date.
*
* @since 4.0.0
*
* @param object $page The additional page object.
* @return string The formatted datetime.
*/
public function lastModifiedAdditionalPage( $page ) {
return aioseo()->helpers->isValidDate( $page->lastModified ) ? gmdate( 'c', strtotime( $page->lastModified ) ) : '';
}
/**
* Returns a list of excluded post IDs.
*
* @since 4.0.0
*
* @return string The excluded IDs.
*/
public function excludedPosts() {
static $excludedPosts = null;
if ( null === $excludedPosts ) {
$excludedPosts = $this->excludedObjectIds( 'excludePosts' );
}
return $excludedPosts;
}
/**
* Returns a list of excluded term IDs.
*
* @since 4.0.0
*
* @return string The excluded IDs.
*/
public function excludedTerms() {
static $excludedTerms = null;
if ( null === $excludedTerms ) {
$excludedTerms = $this->excludedObjectIds( 'excludeTerms' );
}
return $excludedTerms;
}
/**
* Returns a list of excluded IDs for a given option as a comma separated string.
*
* Helper method for excludedPosts() and excludedTerms().
*
* @since 4.0.0
* @version 4.4.7 Improved method name.
*
* @param string $option The option name.
* @return string The excluded IDs.
*/
private function excludedObjectIds( $option ) {
$type = aioseo()->sitemap->type;
// The RSS Sitemap needs to exclude whatever is excluded in the general sitemap.
if ( 'rss' === $type ) {
$type = 'general';
}
// Allow WPML to filter out hidden language posts/terms.
$hiddenObjectIds = [];
if ( aioseo()->helpers->isWpmlActive() ) {
$hiddenLanguages = apply_filters( 'wpml_setting', [], 'hidden_languages' );
foreach ( $hiddenLanguages as $language ) {
$objectTypes = [];
if ( 'excludePosts' === $option ) {
$objectTypes = aioseo()->sitemap->helpers->includedPostTypes();
$objectTypes = array_map( function( $postType ) {
return "post_{$postType}";
}, $objectTypes );
}
if ( 'excludeTerms' === $option ) {
$objectTypes = aioseo()->sitemap->helpers->includedTaxonomies();
$objectTypes = array_map( function( $taxonomy ) {
return "tax_{$taxonomy}";
}, $objectTypes );
}
$dbNoConflict = aioseo()->core->db->noConflict();
$rows = $dbNoConflict->start( 'icl_translations' )
->select( 'element_id' )
->whereIn( 'element_type', $objectTypes )
->where( 'language_code', $language )
->run()
->result();
$ids = array_map( function( $row ) {
return (int) $row->element_id;
}, $rows );
$hiddenObjectIds = array_merge( $hiddenObjectIds, $ids );
}
}
$hasFilter = has_filter( 'aioseo_sitemap_' . aioseo()->helpers->toSnakeCase( $option ) );
$advanced = aioseo()->options->sitemap->$type->advancedSettings->enable;
$excluded = array_merge( $hiddenObjectIds, aioseo()->options->sitemap->{$type}->advancedSettings->{$option} );
if (
! $advanced &&
empty( $excluded ) &&
! $hasFilter
) {
return '';
}
$ids = [];
foreach ( $excluded as $object ) {
if ( is_numeric( $object ) ) {
$ids[] = (int) $object;
continue;
}
$object = json_decode( $object );
if ( is_int( $object->value ) ) {
$ids[] = $object->value;
}
}
if ( 'excludePosts' === $option ) {
$ids = apply_filters( 'aioseo_sitemap_exclude_posts', $ids, $type );
}
if ( 'excludeTerms' === $option ) {
$ids = apply_filters( 'aioseo_sitemap_exclude_terms', $ids, $type );
}
return count( $ids ) ? esc_sql( implode( ', ', $ids ) ) : '';
}
/**
* Returns the URLs of all active sitemaps.
*
* @since 4.0.0
* @version 4.6.2 Removed the prefix from the list of URLs.
*
* @return array $urls The sitemap URLs.
*/
public function getSitemapUrls() {
static $urls = [];
if ( $urls ) {
return $urls;
}
$addonsUrls = array_filter( aioseo()->addons->doAddonFunction( 'helpers', 'getSitemapUrls' ) );
foreach ( $addonsUrls as $addonUrls ) {
$urls = array_merge( $urls, $addonUrls );
}
if ( aioseo()->options->sitemap->general->enable ) {
$urls[] = $this->getUrl( 'general' );
}
if ( aioseo()->options->sitemap->rss->enable ) {
$urls[] = $this->getUrl( 'rss' );
}
return $urls;
}
/**
* Returns the URLs of all active sitemaps with the 'Sitemap: ' prefix.
*
* @since 4.6.2
*
* @return array $urls The sitemap URLs.
*/
public function getSitemapUrlsPrefixed() {
$urls = $this->getSitemapUrls();
foreach ( $urls as &$url ) {
$url = 'Sitemap: ' . $url;
}
return $urls;
}
/**
* Extracts existing sitemap URLs from the robots.txt file.
* We need this in case users have existing sitemap directives added to their robots.txt file.
*
* @since 4.0.10
* @version 4.4.9
*
* @return array The sitemap URLs.
*/
public function extractSitemapUrlsFromRobotsTxt() {
// First, we need to remove our filter, so that it doesn't run unintentionally.
remove_filter( 'robots_txt', [ aioseo()->robotsTxt, 'buildRules' ], 10000 );
$robotsTxt = apply_filters( 'robots_txt', '', true );
add_filter( 'robots_txt', [ aioseo()->robotsTxt, 'buildRules' ], 10000 );
if ( ! $robotsTxt ) {
return [];
}
$lines = explode( "\n", $robotsTxt );
if ( ! is_array( $lines ) || ! count( $lines ) ) {
return [];
}
return aioseo()->robotsTxt->extractSitemapUrls( $robotsTxt );
}
/**
* Returns the URL of the given sitemap type.
*
* @since 4.1.5
*
* @param string $type The sitemap type.
* @return string The sitemap URL.
*/
public function getUrl( $type ) {
$url = home_url( 'sitemap.xml' );
if ( 'rss' === $type ) {
$url = home_url( 'sitemap.rss' );
}
if ( 'general' === $type ) {
// Check if user has a custom filename from the V3 migration.
$filename = $this->filename( 'general' ) ?: 'sitemap';
$url = home_url( $filename . '.xml' );
}
$addon = aioseo()->addons->getLoadedAddon( $type );
if ( ! empty( $addon->helpers ) && method_exists( $addon->helpers, 'getUrl' ) ) {
$url = $addon->helpers->getUrl();
}
return $url;
}
/**
* Returns if images should be excluded from the sitemap.
*
* @since 4.2.2
*
* @return bool
*/
public function excludeImages() {
$shouldExclude = aioseo()->options->sitemap->general->advancedSettings->enable && aioseo()->options->sitemap->general->advancedSettings->excludeImages;
return apply_filters( 'aioseo_sitemap_exclude_images', $shouldExclude );
}
/**
* Returns the post types to check against for the author sitemap.
*
* @since 4.4.4
*
* @return array The post types.
*/
public function getAuthorPostTypes() {
// By default, WP only considers posts for author archives, but users can include additional post types.
$postTypes = [ 'post' ];
return apply_filters( 'aioseo_sitemap_author_post_types', $postTypes );
}
/**
* Decode the Urls from Posts and Terms so they properly show in the Sitemap.
*
* @since 4.6.9
*
* @param mixed $data The data to decode.
* @return array $result The converted data with decoded URLs.
*/
public function decodeSitemapEntries( $data ) {
$result = [];
if ( empty( $data ) ) {
return $result;
}
// Decode Url to properly show Unicode Characters.
foreach ( $data as $item ) {
if ( isset( $item['loc'] ) ) {
$item['loc'] = aioseo()->helpers->decodeUrl( $item['loc'] );
}
// This is for the RSS Sitemap.
if ( isset( $item['guid'] ) ) {
$item['guid'] = aioseo()->helpers->decodeUrl( $item['guid'] );
}
$result[] = $item;
}
return $result;
}
} Sitemap/Query.php 0000666 00000027332 15113050717 0007777 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Utils as CommonUtils;
/**
* Handles all complex queries for the sitemap.
*
* @since 4.0.0
*/
class Query {
/**
* Returns all eligble sitemap entries for a given post type.
*
* @since 4.0.0
*
* @param mixed $postTypes The post type(s). Either a singular string or an array of strings.
* @param array $additionalArgs Any additional arguments for the post query.
* @return array|int The post objects or the post count.
*/
public function posts( $postTypes, $additionalArgs = [] ) {
$includedPostTypes = $postTypes;
$postTypesArray = ! is_array( $postTypes ) ? [ $postTypes ] : $postTypes;
if ( is_array( $postTypes ) ) {
$includedPostTypes = implode( "', '", $postTypes );
}
if (
empty( $includedPostTypes ) ||
( 'attachment' === $includedPostTypes && 'disabled' !== aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls )
) {
return [];
}
// Set defaults.
$maxAge = '';
$fields = implode( ', ', [
'p.ID',
'p.post_excerpt',
'p.post_type',
'p.post_password',
'p.post_parent',
'p.post_date_gmt',
'p.post_modified_gmt',
'ap.priority',
'ap.frequency'
] );
if ( in_array( aioseo()->sitemap->type, [ 'html', 'rss' ], true ) ) {
$fields .= ', p.post_title';
}
if ( 'general' !== aioseo()->sitemap->type || ! aioseo()->sitemap->helpers->excludeImages() ) {
$fields .= ', ap.images';
}
// Order by highest priority first (highest priority at the top),
// then by post modified date (most recently updated at the top).
$orderBy = 'ap.priority DESC, p.post_modified_gmt DESC';
// Override defaults if passed as additional arg.
foreach ( $additionalArgs as $name => $value ) {
// Attachments need to be fetched with all their fields because we need to get their post parent further down the line.
$$name = esc_sql( $value );
if ( 'root' === $name && $value && 'attachment' !== $includedPostTypes ) {
$fields = 'p.ID, p.post_type';
}
if ( 'count' === $name && $value ) {
$fields = 'count(p.ID) as total';
}
}
$query = aioseo()->core->db
->start( aioseo()->core->db->db->posts . ' as p', true )
->select( $fields )
->leftJoin( 'aioseo_posts as ap', 'ap.post_id = p.ID' )
->where( 'p.post_status', 'attachment' === $includedPostTypes ? 'inherit' : 'publish' )
->whereRaw( "p.post_type IN ( '$includedPostTypes' )" );
$homePageId = (int) get_option( 'page_on_front' );
if ( ! is_array( $postTypes ) ) {
if ( ! aioseo()->helpers->isPostTypeNoindexed( $includedPostTypes ) ) {
$query->whereRaw( "( `ap`.`robots_noindex` IS NULL OR `ap`.`robots_default` = 1 OR `ap`.`robots_noindex` = 0 OR post_id = $homePageId )" );
} else {
$query->whereRaw( "( `ap`.`robots_default` = 0 AND `ap`.`robots_noindex` = 0 OR post_id = $homePageId )" );
}
} else {
$robotsMetaSql = [];
foreach ( $postTypes as $postType ) {
if ( ! aioseo()->helpers->isPostTypeNoindexed( $postType ) ) {
$robotsMetaSql[] = "( `p`.`post_type` = '$postType' AND ( `ap`.`robots_noindex` IS NULL OR `ap`.`robots_default` = 1 OR `ap`.`robots_noindex` = 0 OR post_id = $homePageId ) )";
} else {
$robotsMetaSql[] = "( `p`.`post_type` = '$postType' AND ( `ap`.`robots_default` = 0 AND `ap`.`robots_noindex` = 0 OR post_id = $homePageId ) )";
}
}
$query->whereRaw( '( ' . implode( ' OR ', $robotsMetaSql ) . ' )' );
}
$excludedPosts = aioseo()->sitemap->helpers->excludedPosts();
if ( $excludedPosts ) {
$query->whereRaw( "( `p`.`ID` NOT IN ( $excludedPosts ) OR post_id = $homePageId )" );
}
// Exclude posts assigned to excluded terms.
$excludedTerms = aioseo()->sitemap->helpers->excludedTerms();
if ( $excludedTerms ) {
$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
$query->whereRaw("
( `p`.`ID` NOT IN
(
SELECT `tr`.`object_id`
FROM `$termRelationshipsTable` as tr
WHERE `tr`.`term_taxonomy_id` IN ( $excludedTerms )
)
)" );
}
if ( $maxAge ) {
$query->whereRaw( "( `p`.`post_date_gmt` >= '$maxAge' )" );
}
if (
'rss' === aioseo()->sitemap->type ||
(
aioseo()->sitemap->indexes &&
empty( $additionalArgs['root'] ) &&
empty( $additionalArgs['count'] )
)
) {
$query->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset );
}
$isStaticHomepage = 'page' === get_option( 'show_on_front' );
if ( $isStaticHomepage ) {
$excludedPostIds = array_map( 'intval', explode( ',', $excludedPosts ) );
$blogPageId = (int) get_option( 'page_for_posts' );
if ( in_array( 'page', $postTypesArray, true ) ) {
// Exclude the blog page from the pages post type.
if ( $blogPageId ) {
$query->whereRaw( "`p`.`ID` != $blogPageId" );
}
// Custom order by statement to always move the home page to the top.
if ( $homePageId ) {
$orderBy = "case when `p`.`ID` = $homePageId then 0 else 1 end, $orderBy";
}
}
// Include the blog page in the posts post type unless manually excluded.
if (
$blogPageId &&
! in_array( $blogPageId, $excludedPostIds, true ) &&
in_array( 'post', $postTypesArray, true )
) {
// We are using a database class hack to get in an OR clause to
// bypass all the other WHERE statements and just include the
// blog page ID manually.
$query->whereRaw( "1=1 OR `p`.`ID` = $blogPageId" );
// Custom order by statement to always move the blog posts page to the top.
$orderBy = "case when `p`.`ID` = $blogPageId then 0 else 1 end, $orderBy";
}
}
$query->orderByRaw( $orderBy );
$query = $this->filterPostQuery( $query, $postTypes );
// Return the total if we are just counting the posts.
if ( ! empty( $additionalArgs['count'] ) ) {
return (int) $query->run( true, 'var' )
->result();
}
$posts = $query->run()
->result();
// Convert ID from string to int.
foreach ( $posts as $post ) {
$post->ID = (int) $post->ID;
}
return $this->filterPosts( $posts );
}
/**
* Filters the post query.
*
* @since 4.1.4
*
* @param \AIOSEO\Plugin\Common\Utils\Database $query The query.
* @param string $postType The post type.
* @return \AIOSEO\Plugin\Common\Utils\Database The filtered query.
*/
private function filterPostQuery( $query, $postType ) {
switch ( $postType ) {
case 'product':
return $this->excludeHiddenProducts( $query );
default:
break;
}
return $query;
}
/**
* Adds a condition to the query to exclude hidden WooCommerce products.
*
* @since 4.1.4
*
* @param \AIOSEO\Plugin\Common\Utils\Database $query The query.
* @return \AIOSEO\Plugin\Common\Utils\Database The filtered query.
*/
private function excludeHiddenProducts( $query ) {
if (
! aioseo()->helpers->isWooCommerceActive() ||
! apply_filters( 'aioseo_sitemap_woocommerce_exclude_hidden_products', true )
) {
return $query;
}
static $hiddenProductIds = null;
if ( null === $hiddenProductIds ) {
$tempDb = new CommonUtils\Database();
$hiddenProducts = $tempDb->start( 'term_relationships as tr' )
->select( 'tr.object_id' )
->join( 'term_taxonomy as tt', 'tr.term_taxonomy_id = tt.term_taxonomy_id' )
->join( 'terms as t', 'tt.term_id = t.term_id' )
->where( 't.name', 'exclude-from-catalog' )
->run()
->result();
if ( empty( $hiddenProducts ) ) {
return $query;
}
$hiddenProductIds = [];
foreach ( $hiddenProducts as $hiddenProduct ) {
$hiddenProductIds[] = (int) $hiddenProduct->object_id;
}
$hiddenProductIds = esc_sql( implode( ', ', $hiddenProductIds ) );
}
$query->whereRaw( "p.ID NOT IN ( $hiddenProductIds )" );
return $query;
}
/**
* Filters the queried posts.
*
* @since 4.0.0
*
* @param array $posts The posts.
* @return array $remainingPosts The remaining posts.
*/
public function filterPosts( $posts ) {
$remainingPosts = [];
foreach ( $posts as $post ) {
switch ( $post->post_type ) {
case 'attachment':
if ( ! $this->isInvalidAttachment( $post ) ) {
$remainingPosts[] = $post;
}
break;
default:
$remainingPosts[] = $post;
break;
}
}
return $remainingPosts;
}
/**
* Excludes attachments if their post parent isn't published or parent post type isn't registered anymore.
*
* @since 4.0.0
*
* @param Object $post The post.
* @return boolean Whether the attachment is invalid.
*/
private function isInvalidAttachment( $post ) {
if ( empty( $post->post_parent ) ) {
return false;
}
$parent = get_post( $post->post_parent );
if ( ! is_object( $parent ) ) {
return false;
}
if (
'publish' !== $parent->post_status ||
! in_array( $parent->post_type, get_post_types(), true ) ||
$parent->post_password
) {
return true;
}
return false;
}
/**
* Returns all eligible sitemap entries for a given taxonomy.
*
* @since 4.0.0
*
* @param string $taxonomy The taxonomy.
* @param array $additionalArgs Any additional arguments for the term query.
* @return array|int The term objects or the term count.
*/
public function terms( $taxonomy, $additionalArgs = [] ) {
// Set defaults.
$fields = 't.term_id';
$offset = aioseo()->sitemap->offset;
// Override defaults if passed as additional arg.
foreach ( $additionalArgs as $name => $value ) {
$$name = esc_sql( $value );
if ( 'root' === $name && $value ) {
$fields = 't.term_id, tt.count';
}
if ( 'count' === $name && $value ) {
$fields = 'count(t.term_id) as total';
}
}
$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
$termTaxonomyTable = aioseo()->core->db->db->prefix . 'term_taxonomy';
// Include all terms that have assigned posts or whose children have assigned posts.
$query = aioseo()->core->db
->start( aioseo()->core->db->db->terms . ' as t', true )
->select( $fields )
->leftJoin( 'term_taxonomy as tt', '`tt`.`term_id` = `t`.`term_id`' )
->whereRaw( "
( `t`.`term_id` IN
(
SELECT `tt`.`term_id`
FROM `$termTaxonomyTable` as tt
WHERE `tt`.`taxonomy` = '$taxonomy'
AND
(
`tt`.`count` > 0 OR
EXISTS (
SELECT 1
FROM `$termTaxonomyTable` as tt2
WHERE `tt2`.`parent` = `tt`.`term_id`
AND `tt2`.`count` > 0
)
)
)
)" );
$excludedTerms = aioseo()->sitemap->helpers->excludedTerms();
if ( $excludedTerms ) {
$query->whereRaw("
( `t`.`term_id` NOT IN
(
SELECT `tr`.`term_taxonomy_id`
FROM `$termRelationshipsTable` as tr
WHERE `tr`.`term_taxonomy_id` IN ( $excludedTerms )
)
)" );
}
if (
aioseo()->sitemap->indexes &&
empty( $additionalArgs['root'] ) &&
empty( $additionalArgs['count'] )
) {
$query->limit( aioseo()->sitemap->linksPerIndex, $offset );
}
// Return the total if we are just counting the terms.
if ( ! empty( $additionalArgs['count'] ) ) {
return (int) $query->run( true, 'var' )
->result();
}
$terms = $query->orderBy( 't.term_id ASC' )
->run()
->result();
foreach ( $terms as $term ) {
// Convert ID from string to int.
$term->term_id = (int) $term->term_id;
// Add taxonomy name to object manually instead of querying it to prevent redundant join.
$term->taxonomy = $taxonomy;
}
return $terms;
}
/**
* Wipes all data and forces the plugin to rescan the site for images.
*
* @since 4.0.13
*
* @return void
*/
public function resetImages() {
aioseo()->core->db
->update( 'aioseo_posts' )
->set(
[
'images' => null,
'image_scan_date' => null
]
)
->run();
}
} Sitemap/Html/Widget.php 0000666 00000013607 15113050717 0011021 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Widget.
*
* @since 4.1.3
*/
class Widget extends \WP_Widget {
/**
* The default attributes.
*
* @since 4.2.7
*
* @var array
*/
private $defaults = [];
/**
* Class constructor.
*
* @since 4.1.3
*/
public function __construct() {
// The default widget settings.
$this->defaults = [
'title' => '',
'show_label' => 'on',
'archives' => '',
'nofollow_links' => '',
'order' => 'asc',
'order_by' => 'publish_date',
'publication_date' => 'on',
'post_types' => [ 'post', 'page' ],
'taxonomies' => [ 'category', 'post_tag' ],
'excluded_posts' => '',
'excluded_terms' => ''
];
$widgetSlug = 'aioseo-html-sitemap-widget';
$widgetOptions = [
'classname' => $widgetSlug,
// Translators: The short plugin name ("AIOSEO").
'description' => sprintf( esc_html__( '%1$s HTML sitemap widget.', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME )
];
$controlOptions = [
'id_base' => $widgetSlug
];
// Translators: 1 - The plugin short name ("AIOSEO").
$name = sprintf( esc_html__( '%1$s - HTML Sitemap', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );
$name .= ' ' . esc_html__( '(legacy)', 'all-in-one-seo-pack' );
parent::__construct( $widgetSlug, $name, $widgetOptions, $controlOptions );
}
/**
* Callback for the widget.
*
* @since 4.1.3
*
* @param array $args The widget arguments.
* @param array $instance The widget instance options.
* @return void
*/
public function widget( $args, $instance ) {
if ( ! aioseo()->options->sitemap->html->enable ) {
return;
}
// Merge with defaults.
$instance = wp_parse_args( (array) $instance, $this->defaults );
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
if ( ! empty( $instance['title'] ) ) {
echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped,Generic.Files.LineLength.MaxExceeded
}
$instance = aioseo()->htmlSitemap->frontend->getAttributes( $instance );
aioseo()->htmlSitemap->frontend->output( true, $instance );
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Callback to update the widget options.
*
* @since 4.1.3
*
* @param array $newOptions The new options.
* @param array $oldOptions The old options.
* @return array The new options.
*/
public function update( $newOptions, $oldOptions ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$settings = [
'title',
'order',
'order_by',
'show_label',
'publication_date',
'archives',
'excluded_posts',
'excluded_terms'
];
foreach ( $settings as $setting ) {
$newOptions[ $setting ] = ! empty( $newOptions[ $setting ] ) ? wp_strip_all_tags( $newOptions[ $setting ] ) : '';
}
$includedPostTypes = [];
if ( ! empty( $newOptions['post_types'] ) ) {
$postTypes = $this->getPublicPostTypes( true );
foreach ( $newOptions['post_types'] as $v ) {
if ( is_numeric( $v ) ) {
$includedPostTypes[] = $postTypes[ $v ];
} else {
$includedPostTypes[] = $v;
}
}
}
$newOptions['post_types'] = $includedPostTypes;
$includedTaxonomies = [];
if ( ! empty( $newOptions['taxonomies'] ) ) {
$taxonomies = aioseo()->helpers->getPublicTaxonomies( true );
foreach ( $newOptions['taxonomies'] as $v ) {
if ( is_numeric( $v ) ) {
$includedTaxonomies[] = $taxonomies[ $v ];
} else {
$includedTaxonomies[] = $v;
}
}
}
$newOptions['taxonomies'] = $includedTaxonomies;
if ( ! empty( $newOptions['excluded_posts'] ) ) {
$newOptions['excluded_posts'] = $this->sanitizeExcludedIds( $newOptions['excluded_posts'] );
}
if ( ! empty( $newOptions['excluded_terms'] ) ) {
$newOptions['excluded_terms'] = $this->sanitizeExcludedIds( $newOptions['excluded_terms'] );
}
return $newOptions;
}
/**
* Callback for the widgets options form.
*
* @since 4.1.3
*
* @param array $instance The widget options.
* @return void
*/
public function form( $instance ) {
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$instance = wp_parse_args( (array) $instance, $this->defaults );
$postTypeObjects = $this->getPublicPostTypes();
$postTypes = $this->getPublicPostTypes( true );
$taxonomyObjects = aioseo()->helpers->getPublicTaxonomies();
// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
include AIOSEO_DIR . '/app/Common/Views/sitemap/html/widget-options.php';
}
/**
* Returns the public post types (without attachments).
*
* @since 4.1.3
*
* @param boolean $namesOnly Whether only the names should be returned.
* @return array The public post types.
*/
private function getPublicPostTypes( $namesOnly = false ) {
$postTypes = aioseo()->helpers->getPublicPostTypes( $namesOnly );
foreach ( $postTypes as $k => $postType ) {
if ( is_array( $postType ) && 'attachment' === $postType['name'] ) {
unset( $postTypes[ $k ] );
break;
}
if ( ! is_array( $postType ) && 'attachment' === $postType ) {
unset( $postTypes[ $k ] );
break;
}
}
return array_values( $postTypes );
}
/**
* Sanitizes the excluded IDs by removing any non-integer values.
*
* @since 4.1.3
*
* @param string $ids The IDs as a string, comma-separated.
* @return string The sanitized IDs as a string, comma-separated.
*/
private function sanitizeExcludedIds( $ids ) {
$ids = array_map( 'trim', explode( ',', $ids ) );
$ids = array_filter( $ids, 'is_numeric' );
$ids = esc_sql( implode( ', ', $ids ) );
return $ids;
}
} Sitemap/Html/Frontend.php 0000666 00000031777 15113050717 0011365 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the output of the HTML sitemap.
*
* @since 4.1.3
*/
class Frontend {
/**
* Instance of Query class.
*
* @since 4.1.3
*
* @var Query
*/
public $query;
/**
* The attributes for the block/widget/shortcode.
*
* @since 4.1.3
*
* @var array
*/
private $attributes = [];
/**
* Class constructor.
*
* @since 4.1.3
*/
public function __construct() {
$this->query = new Query();
}
/**
* Returns the attributes.
*
* @since 4.1.3
*
* @param array $attributes The user-defined attributes
* @return array The defaults with user-defined attributes merged.
*/
public function getAttributes( $attributes = [] ) {
aioseo()->sitemap->type = 'html';
$defaults = [
'label_tag' => 'h4',
'show_label' => true,
'order' => aioseo()->options->sitemap->html->sortDirection,
'order_by' => aioseo()->options->sitemap->html->sortOrder,
'nofollow_links' => false,
'publication_date' => aioseo()->options->sitemap->html->publicationDate,
'archives' => aioseo()->options->sitemap->html->compactArchives,
'post_types' => aioseo()->sitemap->helpers->includedPostTypes(),
'taxonomies' => aioseo()->sitemap->helpers->includedTaxonomies(),
'excluded_posts' => [],
'excluded_terms' => [],
'is_admin' => false
];
$attributes = shortcode_atts( $defaults, $attributes );
$attributes['show_label'] = filter_var( $attributes['show_label'], FILTER_VALIDATE_BOOLEAN );
$attributes['nofollow_links'] = filter_var( $attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN );
$attributes['is_admin'] = filter_var( $attributes['is_admin'], FILTER_VALIDATE_BOOLEAN );
return $attributes;
}
/**
* Formats the publish date according to what's set under Settings > General.
*
* @since 4.1.3
*
* @param string $date The date that should be formatted.
* @return string The formatted date.
*/
private function formatDate( $date ) {
$dateFormat = apply_filters( 'aioseo_html_sitemap_date_format', get_option( 'date_format' ) );
return date_i18n( $dateFormat, strtotime( $date ) );
}
/**
* Returns the posts of a given post type that should be included.
*
* @since 4.1.3
*
* @param string $postType The post type.
* @param array $additionalArgs Additional arguments for the post query (optional).
* @return array The post entries.
*/
private function posts( $postType, $additionalArgs = [] ) {
$posts = $this->query->posts( $postType, $additionalArgs );
if ( ! $posts ) {
return [];
}
$entries = [];
foreach ( $posts as $post ) {
$entry = [
'id' => $post->ID,
'title' => get_the_title( $post ),
'loc' => get_permalink( $post->ID ),
'date' => $this->formatDate( $post->post_date_gmt ),
'parent' => ! empty( $post->post_parent ) ? $post->post_parent : null
];
$entries[] = $entry;
}
return apply_filters( 'aioseo_html_sitemap_posts', $entries, $postType );
}
/**
* Returns the terms of a given taxonomy that should be included.
*
* @since 4.1.3
*
* @param string $taxonomy The taxonomy name.
* @param array $additionalArgs Additional arguments for the query (optional).
* @return array The term entries.
*/
private function terms( $taxonomy, $additionalArgs = [] ) {
$terms = $this->query->terms( $taxonomy, $additionalArgs );
if ( ! $terms ) {
return [];
}
$entries = [];
foreach ( $terms as $term ) {
$entries[] = [
'id' => $term->term_id,
'title' => $term->name,
'loc' => get_term_link( $term->term_id ),
'parent' => ! empty( $term->parent ) ? $term->parent : null
];
}
return apply_filters( 'aioseo_html_sitemap_terms', $entries, $taxonomy );
}
/**
* Outputs the sitemap to the frontend.
*
* @since 4.1.3
*
* @param bool $echo Whether the sitemap should be printed to the screen.
* @param array $attributes The shortcode attributes.
* @return string|void The HTML sitemap.
*/
public function output( $echo = true, $attributes = [] ) {
$this->attributes = $attributes;
if ( ! aioseo()->options->sitemap->html->enable ) {
return;
}
aioseo()->sitemap->type = 'html';
if ( filter_var( $attributes['archives'], FILTER_VALIDATE_BOOLEAN ) ) {
return ( new CompactArchive() )->output( $attributes, $echo );
}
if ( ! empty( $attributes['default'] ) ) {
$attributes = $this->getAttributes();
}
$noResultsMessage = esc_html__( 'No posts/terms could be found.', 'all-in-one-seo-pack' );
if ( empty( $this->attributes['post_types'] ) && empty( $this->attributes['taxonomies'] ) ) {
if ( $echo ) {
echo $noResultsMessage; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
return $noResultsMessage;
}
// TODO: Consider moving all remaining HTML code below to a dedicated view instead of printing it in PHP.
$sitemap = sprintf(
'<div class="aioseo-html-sitemap%s">',
! $this->attributes['show_label'] ? ' labels-hidden' : ''
);
$sitemap .= '<style>.aioseo-html-sitemap.labels-hidden ul { margin: 0; }</style>';
$hasPosts = false;
$postTypes = $this->getIncludedObjects( $this->attributes['post_types'] );
foreach ( $postTypes as $postType ) {
if ( 'attachment' === $postType ) {
continue;
}
// Check if post type is still registered.
if ( ! in_array( $postType, aioseo()->helpers->getPublicPostTypes( true ), true ) ) {
continue;
}
$posts = $this->posts( $postType, $attributes );
if ( empty( $posts ) ) {
continue;
}
$hasPosts = true;
$postTypeObject = get_post_type_object( $postType );
$label = ! empty( $postTypeObject->label ) ? $postTypeObject->label : ucfirst( $postType );
$sitemap .= '<div class="aioseo-html-' . esc_attr( $postType ) . '-sitemap">';
$sitemap .= $this->generateLabel( $label );
if ( is_post_type_hierarchical( $postType ) ) {
$sitemap .= $this->generateHierarchicalList( $posts ) . '</div>';
if ( $this->attributes['show_label'] ) {
$sitemap .= '<br />';
}
continue;
}
$sitemap .= $this->generateList( $posts );
if ( $this->attributes['show_label'] ) {
$sitemap .= '<br />';
}
}
$hasTerms = false;
$taxonomies = $this->getIncludedObjects( $this->attributes['taxonomies'], false );
foreach ( $taxonomies as $taxonomy ) {
// Check if post type is still registered.
if ( ! in_array( $taxonomy, aioseo()->helpers->getPublicTaxonomies( true ), true ) ) {
continue;
}
$terms = $this->terms( $taxonomy, $attributes );
if ( empty( $terms ) ) {
continue;
}
$hasTerms = true;
$taxonomyObject = get_taxonomy( $taxonomy );
$label = ! empty( $taxonomyObject->label ) ? $taxonomyObject->label : ucfirst( $taxonomy );
$sitemap .= '<div class="aioseo-html-' . esc_attr( $taxonomy ) . '-sitemap">';
$sitemap .= $this->generateLabel( $label );
if ( is_taxonomy_hierarchical( $taxonomy ) ) {
$sitemap .= $this->generateHierarchicalList( $terms ) . '</div>';
if ( $this->attributes['show_label'] ) {
$sitemap .= '<br />';
}
continue;
}
$sitemap .= $this->generateList( $terms );
if ( $this->attributes['show_label'] ) {
$sitemap .= '<br />';
}
}
$sitemap .= '</div>';
// Check if we actually were able to fetch any results.
if ( ! $hasPosts && ! $hasTerms ) {
$sitemap = $noResultsMessage;
}
if ( $echo ) {
echo $sitemap; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
return $sitemap;
}
/**
* Generates the label for a section of the sitemap.
*
* @since 4.1.3
*
* @param string $label The label.
* @return string The HTML code for the label.
*/
private function generateLabel( $label ) {
$labelTag = ! empty( $this->attributes['label_tag'] ) ? $this->attributes['label_tag'] : 'h4';
return $this->attributes['show_label']
? wp_kses_post( sprintf( '<%2$s>%1$s</%2$s>', $label, $labelTag ) )
: '';
}
/**
* Generates the HTML for a non-hierarchical list of objects.
*
* @since 4.1.3
*
* @param array $objects The object.
* @return string The HTML code.
*/
private function generateList( $objects ) {
$list = '<ul>';
foreach ( $objects as $object ) {
$list .= $this->generateListItem( $object ) . '</li>';
}
return $list . '</ul></div>';
}
/**
* Generates a list item for an object (without the closing tag).
* We cannot close it as the caller might need to generate a hierarchical structure inside the list item.
*
* @since 4.1.3
*
* @param array $object The object.
* @return string The HTML code.
*/
private function generateListItem( $object ) {
$li = '';
if ( ! empty( $object['title'] ) ) {
$li .= '<li>';
// add nofollow to the link.
if ( filter_var( $this->attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN ) ) {
$li .= sprintf(
'<a href="%1$s" %2$s %3$s>',
esc_url( $object['loc'] ),
'rel="nofollow"',
$this->attributes['is_admin'] ? 'target="_blank"' : ''
);
} else {
$li .= sprintf(
'<a href="%1$s" %2$s>',
esc_url( $object['loc'] ),
$this->attributes['is_admin'] ? 'target="_blank"' : ''
);
}
$li .= sprintf( '%s', esc_attr( $object['title'] ) );
// add publication date on the list item.
if ( ! empty( $object['date'] ) && filter_var( $this->attributes['publication_date'], FILTER_VALIDATE_BOOLEAN ) ) {
$li .= sprintf( ' (%s)', esc_attr( $object['date'] ) );
}
$li .= '</a>';
}
return $li;
}
/**
* Generates the HTML for a hierarchical list of objects.
*
* @since 4.1.3
*
* @param array $objects The objects.
* @return string The HTML of the hierarchical objects section.
*/
private function generateHierarchicalList( $objects ) {
if ( empty( $objects ) ) {
return '';
}
$objects = $this->buildHierarchicalTree( $objects );
$list = '<ul>';
foreach ( $objects as $object ) {
$list .= $this->generateListItem( $object );
if ( ! empty( $object['children'] ) ) {
$list .= $this->generateHierarchicalTree( $object );
}
$list .= '</li>';
}
$list .= '</ul>';
return $list;
}
/**
* Recursive helper function for generateHierarchicalList().
* Generates hierarchical structure for objects with child objects.
*
* @since 4.1.3
*
* @param array $object The object.
* @return string The HTML code of the hierarchical tree.
*/
private function generateHierarchicalTree( $object ) {
static $nestedLevel = 0;
$tree = '<ul>';
foreach ( $object['children'] as $child ) {
$nestedLevel++;
$tree .= $this->generateListItem( $child );
if ( ! empty( $child['children'] ) ) {
$tree .= $this->generateHierarchicalTree( $child );
}
$tree .= '</li>';
}
$tree .= '</ul>';
return $tree;
}
/**
* Builds the structure for hierarchical objects that have a parent.
*
* @since 4.1.3
* @version 4.2.8
*
* @param array $objects The list of hierarchical objects.
* @return array Multidimensional array with the hierarchical structure.
*/
private function buildHierarchicalTree( $objects ) {
$topLevelIds = [];
$objects = json_decode( wp_json_encode( $objects ) );
foreach ( $objects as $listItem ) {
// Create an array of top level IDs for later reference.
if ( empty( $listItem->parent ) ) {
array_push( $topLevelIds, $listItem->id );
}
// Create an array of children that belong to the current item.
$children = array_filter( $objects, function( $child ) use ( $listItem ) {
if ( ! empty( $child->parent ) ) {
return absint( $child->parent ) === absint( $listItem->id );
}
} );
if ( ! empty( $children ) ) {
$listItem->children = $children;
}
}
// Remove child objects from the root level since they've all been nested.
$objects = array_filter( $objects, function ( $item ) use ( $topLevelIds ) {
return in_array( $item->id, $topLevelIds, true );
} );
return array_values( json_decode( wp_json_encode( $objects ), true ) );
}
/**
* Returns the names of the included post types or taxonomies.
*
* @since 4.1.3
*
* @param array|string $objects The included post types/taxonomies.
* @param boolean $arePostTypes Whether the objects are post types.
* @return array The names of the included post types/taxonomies.
*/
private function getIncludedObjects( $objects, $arePostTypes = true ) {
if ( is_array( $objects ) ) {
return $objects;
}
if ( empty( $objects ) ) {
return [];
}
$exploded = explode( ',', $objects );
$objects = array_map( function( $object ) {
return trim( $object );
}, $exploded );
$publicObjects = $arePostTypes
? aioseo()->helpers->getPublicPostTypes( true )
: aioseo()->helpers->getPublicTaxonomies( true );
$objects = array_filter( $objects, function( $object ) use ( $publicObjects ) {
return in_array( $object, $publicObjects, true );
});
return $objects;
}
} Sitemap/Html/CompactArchive.php 0000666 00000005677 15113050717 0012476 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the Compact Archive's output.
*
* @since 4.1.3
*/
class CompactArchive {
/**
* The shortcode attributes.
*
* @since 4.1.3
*
* @var array
*/
private $attributes;
/**
* Outputs the compact archives sitemap.
*
* @since 4.1.3
*
* @param array $attributes The shortcode attributes.
* @param boolean $echo Whether the HTML code should be printed or returned.
* @return string The HTML for the compact archive.
*/
public function output( $attributes, $echo = true ) {
$dateArchives = ( new Query() )->archives();
$this->attributes = $attributes;
if ( 'asc' === strtolower( $this->attributes['order'] ) ) {
$dateArchives = array_reverse( $dateArchives, true );
}
$data = [
'dateArchives' => $dateArchives,
'lines' => ''
];
foreach ( $dateArchives as $year => $months ) {
$data['lines'] .= $this->generateYearLine( $year, $months ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
ob_start();
aioseo()->templates->getTemplate( 'sitemap/html/compact-archive.php', $data );
$output = ob_get_clean();
if ( $echo ) {
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
return $output;
}
// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
/**
* Generates the HTML for a year line.
*
* @since 4.1.3
*
* @param int $year The year archive.
* @param array $months The month archives for the current year.
* @return string The HTML code for the year.
*/
protected function generateYearLine( $year, $months ) {
$html = '<li><strong><a href="' . get_year_link( $year ) . '">' . esc_html( $year ) . '</a>: </strong> ';
for ( $month = 1; $month <= 12; $month++ ) {
$html .= $this->generateMonth( $year, $months, $month );
}
$html .= '</li>' . "\n";
return wp_kses_post( $html );
}
/**
* Generates the HTML for a month.
*
* @since 4.1.3
*
* @param int $year The year archive.
* @param array $months All month archives for the current year.
* @param int $month The month archive.
* @return string The HTML code for the month.
*/
public function generateMonth( $year, $months, $month ) {
$hasPosts = isset( $months[ $month ] );
$dummyDate = strtotime( "2009/{$month}/25" );
$monthAbbrevation = date_i18n( 'M', $dummyDate );
$html = '<span class="aioseo-empty-month">' . esc_html( $monthAbbrevation ) . '</span> ';
if ( $hasPosts ) {
$noFollow = filter_var( $this->attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN );
$html = sprintf(
'<a href="%1$s" title="%2$s"%3$s>%4$s</a> ',
get_month_link( $year, $month ),
esc_attr( date_i18n( 'F Y', $dummyDate ) ),
$noFollow ? ' rel="nofollow"' : '',
esc_html( $monthAbbrevation )
);
}
return $html;
}
} Sitemap/Html/Sitemap.php 0000666 00000014025 15113050717 0011173 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap\Html {
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Main class for the HTML sitemap.
*
* @since 4.1.3
*/
class Sitemap {
/** Instance of the frontend class.
*
* @since 4.1.3
*
* @var Frontend
*/
public $frontend;
/**
* Instance of the shortcode class.
*
* @since 4.1.3
*
* @var Shortcode
*/
public $shortcode;
/**
* Instance of the block class.
*
* @since 4.1.3
*
* @var Block
*/
public $block;
/**
* Whether the current queried page is the dedicated sitemap page.
*
* @since 4.1.3
*
* @var bool
*/
public $isDedicatedPage = false;
/**
* Class constructor.
*
* @since 4.1.3
*/
public function __construct() {
$this->frontend = new Frontend();
$this->shortcode = new Shortcode();
$this->block = new Block();
add_action( 'widgets_init', [ $this, 'registerWidget' ] );
add_filter( 'aioseo_canonical_url', [ $this, 'getCanonicalUrl' ] );
if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
add_action( 'template_redirect', [ $this, 'checkForDedicatedPage' ] );
}
}
/**
* Register our HTML sitemap widget.
*
* @since 4.1.3
*
* @return void
*/
public function registerWidget() {
if ( aioseo()->helpers->canRegisterLegacyWidget( 'aioseo-html-sitemap-widget' ) ) {
register_widget( 'AIOSEO\Plugin\Common\Sitemap\Html\Widget' );
}
}
/**
* Checks whether the current request is for our dedicated HTML sitemap page.
*
* @since 4.1.3
*
* @return void
*/
public function checkForDedicatedPage() {
if ( ! aioseo()->options->sitemap->html->enable ) {
return;
}
global $wp;
$sitemapUrl = aioseo()->options->sitemap->html->pageUrl;
if ( ! $sitemapUrl || empty( $wp->request ) ) {
return;
}
$sitemapUrl = wp_parse_url( $sitemapUrl );
if ( empty( $sitemapUrl['path'] ) ) {
return;
}
$sitemapUrl = trim( $sitemapUrl['path'], '/' );
if ( trim( $wp->request, '/' ) === $sitemapUrl ) {
$this->isDedicatedPage = true;
$this->generatePage();
}
}
/**
* Checks whether the current request is for our dedicated HTML sitemap page.
*
* @since 4.1.3
*
* @return void
*/
private function generatePage() {
global $wp_query, $wp, $post; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
$postId = -1337; // Set a negative ID to prevent conflicts with existing posts.
$sitemapUrl = aioseo()->options->sitemap->html->pageUrl;
$path = trim( wp_parse_url( $sitemapUrl )['path'], '/' );
$fakePost = new \stdClass();
$fakePost->ID = $postId;
$fakePost->post_author = 1;
$fakePost->post_date = current_time( 'mysql' );
$fakePost->post_date_gmt = current_time( 'mysql', 1 );
$fakePost->post_title = apply_filters( 'aioseo_html_sitemap_page_title', __( 'Sitemap', 'all-in-one-seo-pack' ) );
$fakePost->post_content = '[aioseo_html_sitemap archives=false]';
// We're using post instead of page to prevent calls to get_ancestors(), which will trigger errors.
// To loead the page template, we set is_page to true on the WP_Query object.
$fakePost->post_type = 'post';
$fakePost->post_status = 'publish';
$fakePost->comment_status = 'closed';
$fakePost->ping_status = 'closed';
$fakePost->post_name = $path;
$fakePost->filter = 'raw'; // Needed to prevent calls to the database when creating the WP_Post object.
$postObject = new \WP_Post( $fakePost );
$post = $postObject;
// We'll set as much properties on the WP_Query object as we can to prevent conflicts with other plugins/themes.
// phpcs:disable Squiz.NamingConventions.ValidVariableName
$wp_query->is_404 = false;
$wp_query->is_page = true;
$wp_query->is_singular = true;
$wp_query->post = $postObject;
$wp_query->posts = [ $postObject ];
$wp_query->queried_object = $postObject;
$wp_query->queried_object_id = $postId;
$wp_query->found_posts = 1;
$wp_query->post_count = 1;
$wp_query->max_num_pages = 1;
unset( $wp_query->query['error'] );
$wp_query->query_vars['error'] = '';
// phpcs:enable Squiz.NamingConventions.ValidVariableName
// We need to add the post object to the cache so that get_post() calls don't trigger database calls.
wp_cache_add( $postId, $postObject, 'posts' );
$GLOBALS['wp_query'] = $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
$wp->register_globals();
// Setting is_404 is not sufficient, so we still need to change the status code.
status_header( 200 );
}
/**
* Get the canonical URL for the dedicated HTML sitemap page.
*
* @since 4.5.7
*
* @param string $originalUrl The canonical URL.
* @return string The canonical URL.
*/
public function getCanonicalUrl( $originalUrl ) {
$sitemapOptions = aioseo()->options->sitemap->html;
if ( ! $sitemapOptions->enable || ! $this->isDedicatedPage ) {
return $originalUrl;
}
// If the user has set a custom URL for the sitemap page, use that.
if ( $sitemapOptions->pageUrl ) {
return $sitemapOptions->pageUrl;
}
// Return the current URL of WP.
global $wp;
return home_url( $wp->request );
}
}
}
namespace {
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! function_exists( 'aioseo_html_sitemap' ) ) {
/**
* Global function that can be used to print the HTML sitemap.
*
* @since 4.1.3
*
* @param array $attributes User-defined attributes that override the default settings.
* @param boolean $echo Whether to echo the output or return it.
* @return string The HTML sitemap code.
*/
function aioseo_html_sitemap( $attributes = [], $echo = true ) {
$attributes = aioseo()->htmlSitemap->frontend->getAttributes( $attributes );
return aioseo()->htmlSitemap->frontend->output( $echo, $attributes );
}
}
} Sitemap/Html/Query.php 0000666 00000014575 15113050717 0010710 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles all queries for the HTML sitemap.
*
* @since 4.1.3
*/
class Query {
/**
* Returns all eligible sitemap entries for a given post type.
*
* @since 4.1.3
*
* @param string $postType The post type.
* @param array $attributes The attributes.
* @return array The post objects.
*/
public function posts( $postType, $attributes ) {
$fields = '`ID`, `post_title`,';
$fields .= '`post_parent`, `post_date_gmt`, `post_modified_gmt`';
$orderBy = '';
switch ( $attributes['order_by'] ) {
case 'last_updated':
$orderBy = 'post_modified_gmt';
break;
case 'alphabetical':
$orderBy = 'post_title';
break;
case 'id':
$orderBy = 'ID';
break;
case 'publish_date':
default:
$orderBy = 'post_date_gmt';
break;
}
switch ( strtolower( $attributes['order'] ) ) {
case 'desc':
$orderBy .= ' DESC';
break;
default:
$orderBy .= ' ASC';
}
$query = aioseo()->core->db
->start( 'posts' )
->select( $fields )
->where( 'post_status', 'publish' )
->where( 'post_type', $postType );
$excludedPosts = $this->getExcludedObjects( $attributes );
if ( $excludedPosts ) {
$query->whereRaw( "( `ID` NOT IN ( $excludedPosts ) )" );
}
$posts = $query->orderBy( $orderBy )
->run()
->result();
foreach ( $posts as $post ) {
$post->ID = (int) $post->ID;
}
return $posts;
}
/**
* Returns all eligble sitemap entries for a given taxonomy.
*
* @since 4.1.3
*
* @param string $taxonomy The taxonomy name.
* @param array $attributes The attributes.
* @return array The term objects.
*/
public function terms( $taxonomy, $attributes = [] ) {
$fields = 't.term_id, t.name, tt.parent';
$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
$termTaxonomyTable = aioseo()->core->db->db->prefix . 'term_taxonomy';
$orderBy = '';
switch ( $attributes['order_by'] ) {
case 'alphabetical':
$orderBy = 't.name';
break;
// We can only sort by date after getting the terms.
case 'id':
case 'publish_date':
case 'last_updated':
default:
$orderBy = 't.term_id';
break;
}
switch ( strtolower( $attributes['order'] ) ) {
case 'desc':
$orderBy .= ' DESC';
break;
default:
$orderBy .= ' ASC';
}
$query = aioseo()->core->db
->start( 'terms as t' )
->select( $fields )
->join( 'term_taxonomy as tt', 't.term_id = tt.term_id' )
->whereRaw( "
( `t`.`term_id` IN
(
SELECT `tt`.`term_id`
FROM `$termTaxonomyTable` as tt
WHERE `tt`.`taxonomy` = '$taxonomy'
AND `tt`.`count` > 0
)
)" );
$excludedTerms = $this->getExcludedObjects( $attributes, false );
if ( $excludedTerms ) {
$query->whereRaw("
( `t`.`term_id` NOT IN
(
SELECT `tr`.`term_taxonomy_id`
FROM `$termRelationshipsTable` as tr
WHERE `tr`.`term_taxonomy_id` IN ( $excludedTerms )
)
)" );
}
$terms = $query->orderBy( $orderBy )
->run()
->result();
foreach ( $terms as $term ) {
$term->term_id = (int) $term->term_id;
$term->taxonomy = $taxonomy;
}
$shouldSort = false;
if ( 'last_updated' === $attributes['order_by'] ) {
$shouldSort = true;
foreach ( $terms as $term ) {
$term->timestamp = strtotime( aioseo()->sitemap->content->getTermLastModified( $term->term_id ) );
}
}
if ( 'publish_date' === $attributes['order_by'] ) {
$shouldSort = true;
foreach ( $terms as $term ) {
$term->timestamp = strtotime( $this->getTermPublishDate( $term->term_id ) );
}
}
if ( $shouldSort ) {
if ( 'asc' === strtolower( $attributes['order'] ) ) {
usort( $terms, function( $term1, $term2 ) {
return $term1->timestamp > $term2->timestamp ? 1 : 0;
} );
} else {
usort( $terms, function( $term1, $term2 ) {
return $term1->timestamp < $term2->timestamp ? 1 : 0;
} );
}
}
return $terms;
}
/**
* Returns a list of date archives that can be included.
*
* @since 4.1.3
*
* @return array The date archives.
*/
public function archives() {
$result = aioseo()->core->db
->start( 'posts', false, 'SELECT DISTINCT' )
->select( 'YEAR(post_date) AS year, MONTH(post_date) AS month' )
->where( 'post_type', 'post' )
->where( 'post_status', 'publish' )
->whereRaw( "post_password=''" )
->orderBy( 'year DESC' )
->orderBy( 'month DESC' )
->run()
->result();
$dates = [];
foreach ( $result as $date ) {
$dates[ $date->year ][ $date->month ] = 1;
}
return $dates;
}
/**
* Returns the publish date for a given term.
* This is the publish date of the oldest post that is assigned to the term.
*
* @since 4.1.3
*
* @param int $termId The term ID.
* @return int The publish date timestamp.
*/
public function getTermPublishDate( $termId ) {
$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
$post = aioseo()->core->db
->start( 'posts as p' )
->select( 'MIN(`p`.`post_date_gmt`) as publish_date' )
->whereRaw( "
( `p`.`ID` IN
(
SELECT `tr`.`object_id`
FROM `$termRelationshipsTable` as tr
WHERE `tr`.`term_taxonomy_id` = '$termId'
)
)" )
->run()
->result();
return ! empty( $post[0]->publish_date ) ? strtotime( $post[0]->publish_date ) : 0;
}
/**
* Returns a comma-separated string of excluded object IDs.
*
* @since 4.1.3
*
* @param array $attributes The attributes.
* @param boolean $posts Whether the objects are posts.
* @return string The excluded object IDs.
*/
private function getExcludedObjects( $attributes, $posts = true ) {
$excludedObjects = $posts
? aioseo()->sitemap->helpers->excludedPosts()
: aioseo()->sitemap->helpers->excludedTerms();
$key = $posts ? 'excluded_posts' : 'excluded_terms';
if ( ! empty( $attributes[ $key ] ) ) {
$ids = explode( ',', $excludedObjects );
$extraIds = [];
if ( is_array( $attributes[ $key ] ) ) {
$extraIds = $attributes[ $key ];
}
if ( is_string( $attributes[ $key ] ) ) {
$extraIds = array_map( 'trim', explode( ',', $attributes[ $key ] ) );
}
$ids = array_filter( array_merge( $ids, $extraIds ), 'is_numeric' );
$excludedObjects = esc_sql( implode( ', ', $ids ) );
}
return $excludedObjects;
}
} Sitemap/Html/Shortcode.php 0000666 00000001336 15113050717 0011524 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the HTML sitemap shortcode.
*
* @since 4.1.3
*/
class Shortcode {
/**
* Class constructor.
*
* @since 4.1.3
*/
public function __construct() {
add_shortcode( 'aioseo_html_sitemap', [ $this, 'render' ] );
}
/**
* Shortcode callback.
*
* @since 4.1.3
*
* @param array $attributes The shortcode attributes.
* @return string|void The HTML sitemap.
*/
public function render( $attributes ) {
$attributes = aioseo()->htmlSitemap->frontend->getAttributes( $attributes );
return aioseo()->htmlSitemap->frontend->output( false, $attributes );
}
} Sitemap/Html/Block.php 0000666 00000006636 15113050717 0010634 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap\Html;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the HTML sitemap block.
*
* @since 4.1.3
*/
class Block {
/**
* Class constructor.
*
* @since 4.1.1
*/
public function __construct() {
add_action( 'init', [ $this, 'register' ] );
}
/**
* Registers the block.
*
* @since 4.1.3
*
* @return void
*/
public function register() {
aioseo()->blocks->registerBlock(
'aioseo/html-sitemap', [
'attributes' => [
'default' => [
'type' => 'boolean',
'default' => true
],
'post_types' => [
'type' => 'string',
'default' => wp_json_encode( [ 'post', 'page' ] )
],
'post_types_all' => [
'type' => 'boolean',
'default' => true
],
'taxonomies' => [
'type' => 'string',
'default' => wp_json_encode( [ 'category', 'post_tag' ] )
],
'taxonomies_all' => [
'type' => 'boolean',
'default' => true
],
'show_label' => [
'type' => 'boolean',
'default' => true
],
'archives' => [
'type' => 'boolean',
'default' => false
],
'publication_date' => [
'type' => 'boolean',
'default' => true
],
'nofollow_links' => [
'type' => 'boolean',
'default' => false
],
'order_by' => [
'type' => 'string',
'default' => 'publish_date'
],
'order' => [
'type' => 'string',
'default' => 'asc'
],
'excluded_posts' => [
'type' => 'string',
'default' => wp_json_encode( [] )
],
'excluded_terms' => [
'type' => 'string',
'default' => wp_json_encode( [] )
],
'is_admin' => [
'type' => 'boolean',
'default' => false
]
],
'render_callback' => [ $this, 'render' ],
'editor_style' => 'aioseo-html-sitemap'
]
);
}
/**
* Renders the block.
*
* @since 4.1.3
*
* @param array $attributes The attributes.
* @return string The HTML sitemap code.
*/
public function render( $attributes ) {
if ( ! $attributes['default'] ) {
$jsonFields = [ 'post_types', 'taxonomies', 'excluded_posts', 'excluded_terms' ];
foreach ( $attributes as $k => $v ) {
if ( in_array( $k, $jsonFields, true ) ) {
$attributes[ $k ] = json_decode( $v );
}
}
$attributes['excluded_posts'] = $this->extractIds( $attributes['excluded_posts'] );
$attributes['excluded_terms'] = $this->extractIds( $attributes['excluded_terms'] );
if ( ! empty( $attributes['post_types_all'] ) ) {
$attributes['post_types'] = aioseo()->helpers->getPublicPostTypes( true );
}
if ( ! empty( $attributes['taxonomies_all'] ) ) {
$attributes['taxonomies'] = aioseo()->helpers->getPublicTaxonomies( true );
}
} else {
$attributes = [];
}
$attributes = aioseo()->htmlSitemap->frontend->getAttributes( $attributes );
return aioseo()->htmlSitemap->frontend->output( false, $attributes );
}
/**
* Extracts the IDs from the excluded objects.
*
* @since 4.1.3
*
* @param array $objects The objects.
* @return array The object IDs.
*/
private function extractIds( $objects ) {
return array_map( function ( $object ) {
$object = json_decode( $object );
return (int) $object->value;
}, $objects );
}
} Sitemap/Localization.php 0000666 00000024757 15113050717 0011332 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles sitemap localization logic.
*
* @since 4.2.1
*/
class Localization {
/**
* This is cached so we don't do the lookup each query.
*
* @since 4.0.0
*
* @var boolean
*/
private static $wpml = null;
/**
* Class constructor.
*
* @since 4.2.1
*/
public function __construct() {
if ( apply_filters( 'aioseo_sitemap_localization_disable', false ) ) {
return;
}
if ( aioseo()->helpers->isWpmlActive() ) {
self::$wpml = [
'defaultLanguage' => apply_filters( 'wpml_default_language', null ),
'activeLanguages' => apply_filters( 'wpml_active_languages', null )
];
add_filter( 'aioseo_sitemap_term', [ $this, 'localizeWpml' ], 10, 4 );
add_filter( 'aioseo_sitemap_post', [ $this, 'localizeWpml' ], 10, 4 );
}
if ( aioseo()->helpers->isPluginActive( 'weglot' ) ) {
add_filter( 'aioseo_sitemap_term', [ $this, 'localizeWeglot' ], 10, 4 );
add_filter( 'aioseo_sitemap_post', [ $this, 'localizeWeglot' ], 10, 4 );
add_filter( 'aioseo_sitemap_author_entry', [ $this, 'localizeWeglot' ], 10, 4 );
add_filter( 'aioseo_sitemap_archive_entry', [ $this, 'localizeWeglot' ], 10, 4 );
add_filter( 'aioseo_sitemap_date_entry', [ $this, 'localizeWeglot' ], 10, 4 );
add_filter( 'aioseo_sitemap_product_attributes', [ $this, 'localizeWeglot' ], 10, 4 );
}
}
/**
* Localize the entries for Weglot.
*
* @since 4.8.3
*
* @param array $entry The entry.
* @param mixed $entryId The object ID, null or a date object.
* @param string $objectName The post type, taxonomy name or date type ('year' or 'month').
* @param string|null $entryType Whether the entry represents a post, term, author, archive or date.
* @return array The entry.
*/
public function localizeWeglot( $entry, $entryId, $objectName, $entryType = null ) {
try {
$originalLang = function_exists( 'weglot_get_original_language' ) ? weglot_get_original_language() : '';
$translations = function_exists( 'weglot_get_destination_languages' ) ? weglot_get_destination_languages() : [];
if ( empty( $originalLang ) || empty( $translations ) ) {
return $entry;
}
switch ( $entryType ) {
case 'post':
$permalink = get_permalink( $entryId );
break;
case 'term':
$permalink = get_term_link( $entryId, $objectName );
break;
case 'author':
$permalink = get_author_posts_url( $entryId, $objectName );
break;
case 'archive':
$permalink = get_post_type_archive_link( $objectName );
break;
case 'date':
$permalink = 'year' === $objectName ? get_year_link( $entryId->year ) : get_month_link( $entryId->year, $entryId->month );
break;
default:
$permalink = '';
}
$entry['languages'] = [];
foreach ( $translations as $translation ) {
// If the translation is not public we skip it.
if ( empty( $translation['public'] ) ) {
continue;
}
$l10nPermalink = $this->weglotGetLocalizedUrl( $permalink, $translation['language_to'] );
if ( ! empty( $l10nPermalink ) ) {
$entry['languages'][] = [
'language' => $translation['language_to'],
'location' => $l10nPermalink
];
}
}
// Also include the main page as a translated variant, per Google's specifications, but only if we found at least one other language.
if ( ! empty( $entry['languages'] ) ) {
$entry['languages'][] = [
'language' => $originalLang,
'location' => aioseo()->helpers->decodeUrl( $entry['loc'] )
];
} else {
unset( $entry['languages'] );
}
return $this->validateSubentries( $entry );
} catch ( \Exception $e ) {
// Do nothing. It only exists because some "weglot" functions above throw exceptions.
}
return $entry;
}
/**
* Localize the entries for WPML.
*
* @since 4.0.0
* @version 4.8.3 Rename from localizeEntry to localizeWpml.
*
* @param array $entry The entry.
* @param int $entryId The post/term ID.
* @param string $objectName The post type or taxonomy name.
* @param string $objectType Whether the entry is a post or term.
* @return array The entry.
*/
public function localizeWpml( $entry, $entryId, $objectName, $objectType ) {
$elementId = $entryId;
$elementType = 'post_' . $objectName;
if ( 'term' === $objectType ) {
$term = aioseo()->helpers->getTerm( $entryId, $objectName );
$elementId = $term->term_taxonomy_id;
$elementType = 'tax_' . $objectName;
}
$translationGroupId = apply_filters( 'wpml_element_trid', null, $elementId, $elementType );
$translations = apply_filters( 'wpml_get_element_translations', null, $translationGroupId, $elementType );
if ( empty( $translations ) ) {
return $entry;
}
$entry['languages'] = [];
$hiddenLanguages = apply_filters( 'wpml_setting', [], 'hidden_languages' );
foreach ( $translations as $translation ) {
if (
empty( $translation->element_id ) ||
! isset( self::$wpml['activeLanguages'][ $translation->language_code ] ) ||
in_array( $translation->language_code, $hiddenLanguages, true )
) {
continue;
}
$currentLanguage = ! empty( self::$wpml['activeLanguages'][ $translation->language_code ] ) ? self::$wpml['activeLanguages'][ $translation->language_code ] : null;
$languageCode = ! empty( $currentLanguage['tag'] ) ? $currentLanguage['tag'] : $translation->language_code;
if ( (int) $elementId === (int) $translation->element_id ) {
$entry['language'] = $languageCode;
continue;
}
$translatedObjectId = apply_filters( 'wpml_object_id', $entryId, $objectName, false, $translation->language_code );
if (
( 'post' === $objectType && $this->isExcludedPost( $translatedObjectId ) ) ||
( 'term' === $objectType && $this->isExcludedTerm( $translatedObjectId ) )
) {
continue;
}
if ( 'post' === $objectType ) {
$permalink = get_permalink( $translatedObjectId );
// Special treatment for the home page translations.
if ( 'page' === get_option( 'show_on_front' ) && aioseo()->helpers->wpmlIsHomePage( $entryId ) ) {
$permalink = aioseo()->helpers->wpmlHomeUrl( $translation->language_code );
}
} else {
$permalink = get_term_link( $translatedObjectId, $objectName );
}
if ( ! empty( $languageCode ) && ! empty( $permalink ) ) {
$entry['languages'][] = [
'language' => $languageCode,
'location' => aioseo()->helpers->decodeUrl( $permalink )
];
}
}
// Also include the main page as a translated variant, per Google's specifications, but only if we found at least one other language.
if ( ! empty( $entry['language'] ) && ! empty( $entry['languages'] ) ) {
$entry['languages'][] = [
'language' => $entry['language'],
'location' => aioseo()->helpers->decodeUrl( $entry['loc'] )
];
} else {
unset( $entry['languages'] );
}
return $this->validateSubentries( $entry );
}
/**
* Validates the subentries with translated variants to ensure all required values are set.
*
* @since 4.2.3
*
* @param array $entry The entry.
* @return array The validated entry.
*/
private function validateSubentries( $entry ) {
if ( ! isset( $entry['languages'] ) ) {
return $entry;
}
foreach ( $entry['languages'] as $index => $subentry ) {
if ( empty( $subentry['language'] ) || empty( $subentry['location'] ) ) {
unset( $entry['languages'][ $index ] );
}
}
return $entry;
}
/**
* Checks whether the given post should be excluded.
*
* @since 4.2.4
*
* @param int $postId The post ID.
* @return bool Whether the post should be excluded.
*/
private function isExcludedPost( $postId ) {
static $excludedPostIds = null;
if ( null === $excludedPostIds ) {
$excludedPostIds = explode( ', ', aioseo()->sitemap->helpers->excludedPosts() );
$excludedPostIds = array_map( function ( $postId ) {
return (int) $postId;
}, $excludedPostIds );
}
if ( in_array( $postId, $excludedPostIds, true ) ) {
return true;
}
// Let's also check if the post is published and not password-protected.
$post = get_post( $postId );
if ( ! is_a( $post, 'WP_Post' ) ) {
return true;
}
if ( ! empty( $post->post_password ) || 'publish' !== $post->post_status ) {
return true;
}
// Now, we must also check for noindex.
$metaData = aioseo()->meta->metaData->getMetaData( $post );
if ( ! empty( $metaData->robots_noindex ) ) {
return true;
}
return false;
}
/**
* Checks whether the given term should be excluded.
*
* @since 4.2.4
*
* @param int $termId The term ID.
* @return bool Whether the term should be excluded.
*/
private function isExcludedTerm( $termId ) {
static $excludedTermIds = null;
if ( null === $excludedTermIds ) {
$excludedTermIds = explode( ', ', aioseo()->sitemap->helpers->excludedTerms() );
$excludedTermIds = array_map( function ( $termId ) {
return (int) $termId;
}, $excludedTermIds );
}
if ( in_array( $termId, $excludedTermIds, true ) ) {
return true;
}
// Now, we must also check for noindex.
$term = aioseo()->helpers->getTerm( $termId );
if ( ! is_a( $term, 'WP_Term' ) ) {
return true;
}
// At least one post must be assigned to the term.
$posts = aioseo()->core->db->start( 'term_relationships' )
->select( 'object_id' )
->where( 'term_taxonomy_id =', $term->term_taxonomy_id )
->limit( 1 )
->run()
->result();
if ( empty( $posts ) ) {
return true;
}
$metaData = aioseo()->meta->metaData->getMetaData( $term );
if ( ! empty( $metaData->robots_noindex ) ) {
return true;
}
return false;
}
/**
* Retrieves the localized URL.
*
* @since 4.8.3
*
* @param string $url The page URL to localize.
* @param string $code The language code (e.g. 'br', 'en').
* @return string|false The localized URL or false if it fails.
*/
private function weglotGetLocalizedUrl( $url, $code ) {
try {
if (
! $url ||
! function_exists( 'weglot_get_service' )
) {
return false;
}
$languageService = weglot_get_service( 'Language_Service_Weglot' );
$requestUrlService = weglot_get_service( 'Request_Url_Service_Weglot' );
$wgUrl = $requestUrlService->create_url_object( $url );
$language = $languageService->get_language_from_internal( $code );
return $wgUrl->getForLanguage( $language );
} catch ( \Exception $e ) {
// Do nothing. It only exists because some "weglot" functions above throw exceptions.
}
return false;
}
} Sitemap/Output.php 0000666 00000011644 15113050717 0010171 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles outputting the sitemap.
*
* @since 4.0.0
*/
class Output {
/**
* Outputs the sitemap.
*
* @since 4.0.0
*
* @param array $entries The sitemap entries.
* @return void
*/
public function output( $entries ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! in_array( aioseo()->sitemap->type, [ 'general', 'rss' ], true ) ) {
return;
}
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$entries = aioseo()->sitemap->helpers->decodeSitemapEntries( $entries );
$charset = aioseo()->helpers->getCharset();
$excludeImages = aioseo()->sitemap->helpers->excludeImages();
$generation = ! isset( aioseo()->sitemap->isStatic ) || aioseo()->sitemap->isStatic ? __( 'statically', 'all-in-one-seo-pack' ) : __( 'dynamically', 'all-in-one-seo-pack' );
$version = aioseo()->helpers->getAioseoVersion();
if ( ! empty( $version ) ) {
$version = 'v' . $version;
}
// Clear all output buffers to avoid conflicts.
aioseo()->helpers->clearBuffers();
echo '<?xml version="1.0" encoding="' . esc_attr( $charset ) . "\"?>\r\n";
echo '<!-- ' . sprintf(
// Translators: 1 - "statically" or "dynamically", 2 - The date, 3 - The time, 4 - The plugin name ("All in One SEO"), 5 - Currently installed version.
esc_html__( 'This sitemap was %1$s generated on %2$s at %3$s by %4$s %5$s - the original SEO plugin for WordPress.', 'all-in-one-seo-pack' ),
esc_html( $generation ),
esc_html( date_i18n( get_option( 'date_format' ) ) ),
esc_html( date_i18n( get_option( 'time_format' ) ) ),
esc_html( AIOSEO_PLUGIN_NAME ),
esc_html( $version )
) . ' -->';
if ( 'rss' === aioseo()->sitemap->type ) {
$xslUrl = home_url() . '/default-sitemap.xsl';
if ( ! is_multisite() ) {
$title = get_bloginfo( 'name' );
$description = get_bloginfo( 'blogdescription' );
$link = home_url();
} else {
$title = get_blog_option( get_current_blog_id(), 'blogname' );
$description = get_blog_option( get_current_blog_id(), 'blogdescription' );
$link = get_blog_option( get_current_blog_id(), 'siteurl' );
}
$ttl = apply_filters( 'aioseo_sitemap_rss_ttl', 60 );
echo "\r\n\r\n<?xml-stylesheet type=\"text/xsl\" href=\"" . esc_url( $xslUrl ) . "\"?>\r\n";
include_once AIOSEO_DIR . '/app/Common/Views/sitemap/xml/rss.php';
return;
}
if ( 'root' === aioseo()->sitemap->indexName && aioseo()->sitemap->indexes ) {
$xslUrl = add_query_arg( 'sitemap', aioseo()->sitemap->indexName, home_url() . '/default-sitemap.xsl' );
echo "\r\n\r\n<?xml-stylesheet type=\"text/xsl\" href=\"" . esc_url( $xslUrl ) . "\"?>\r\n";
include AIOSEO_DIR . '/app/Common/Views/sitemap/xml/root.php';
return;
}
$xslUrl = add_query_arg( 'sitemap', aioseo()->sitemap->indexName, home_url() . '/default-sitemap.xsl' );
echo "\r\n\r\n<?xml-stylesheet type=\"text/xsl\" href=\"" . esc_url( $xslUrl ) . "\"?>\r\n";
include AIOSEO_DIR . '/app/Common/Views/sitemap/xml/default.php';
}
/**
* Escapes and echoes the given XML tag value.
*
* @since 4.0.0
*
* @param string $value The tag value.
* @param bool $wrap Whether the value should we wrapped in a CDATA section.
* @return void
*/
public function escapeAndEcho( $value, $wrap = true ) {
$safeText = is_string( $value ) ? wp_check_invalid_utf8( $value, true ) : $value;
$isZero = is_numeric( $value ) ? 0 === (int) $value : false;
if ( ! $safeText && ! $isZero ) {
return;
}
$cdataRegex = '\<\!\[CDATA\[.*?\]\]\>';
$regex = "/(?=.*?{$cdataRegex})(?<non_cdata_followed_by_cdata>(.*?))(?<cdata>({$cdataRegex}))|(?<non_cdata>(.*))/sx";
$safeText = (string) preg_replace_callback(
$regex,
static function( $matches ) {
if ( ! $matches[0] ) {
return '';
}
if ( ! empty( $matches['non_cdata'] ) ) {
// Escape HTML entities in the non-CDATA section.
return _wp_specialchars( $matches['non_cdata'], ENT_XML1 );
}
// Return the CDATA Section unchanged, escape HTML entities in the rest.
return _wp_specialchars( $matches['non_cdata_followed_by_cdata'], ENT_XML1 ) . $matches['cdata'];
},
$safeText
);
$safeText = $safeText ? $safeText : ( $isZero ? $value : '' );
if ( ! $wrap ) {
return print( $safeText ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
printf( '<![CDATA[%1$s]]>', $safeText ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Returns the URL for the sitemap stylesheet.
*
* This is needed for compatibility with multilingual plugins such as WPML.
*
* @since 4.0.0
*
* @return string The URL to the sitemap stylesheet.
*/
private function xslUrl() {
return esc_url( apply_filters( 'aioseo_sitemap_xsl_url', aioseo()->helpers->localizedUrl( '/sitemap.xsl' ) ) );
}
} Sitemap/Sitemap.php 0000666 00000025377 15113050717 0010303 0 ustar 00 <?php
namespace AIOSEO\Plugin\Common\Sitemap;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* Handles our sitemaps.
*
* @since 4.0.0
*/
class Sitemap extends SitemapAbstract {
/**
* The sitemap filename.
*
* @since 4.4.2
*
* @var string
*/
public $filename = '';
/**
* Whether the sitemap indexes are enabled.
*
* @since 4.4.2
*
* @var bool
*/
public $indexes = false;
/**
* The sitemap index name.
*
* @since 4.4.2
*
* @var string
*/
public $indexName = '';
/**
* The number of links per index.
*
* @since 4.4.2
*
* @var int
*/
public $linksPerIndex = 1000;
/**
* The current page number.
*
* @since 4.4.2
*
* @var int
*/
public $pageNumber = 0;
/**
* The entries' offset.
*
* @since 4.4.2
*
* @var int
*/
public $offset = 0;
/**
* Whether the sitemap is static.
*
* @since 4.4.2
*
* @var bool
*/
public $isStatic = false;
/**
* Class constructor.
*
* @since 4.0.0
*/
public function __construct() {
$this->content = new Content();
$this->root = new Root();
$this->query = new Query();
$this->file = new File();
$this->image = new Image\Image();
$this->priority = new Priority();
$this->output = new Output();
$this->helpers = new Helpers();
$this->requestParser = new RequestParser();
$this->xsl = new Xsl();
new Localization();
$this->disableWpSitemap();
}
/**
* Adds our hooks.
* Note: This runs init and is triggered in the main AIOSEO class.
*
* @since 4.0.0
*
* @return void
*/
public function init() {
add_action( 'aioseo_static_sitemap_regeneration', [ $this, 'regenerateStaticSitemap' ] );
// Check if static files need to be updated.
add_action( 'wp_insert_post', [ $this, 'regenerateOnUpdate' ] );
add_action( 'edited_term', [ $this, 'regenerateStaticSitemap' ] );
add_action( 'admin_init', [ $this, 'detectStatic' ] );
$this->maybeAddHtaccessRewriteRules();
}
/**
* Disables the WP Core sitemap if our general sitemap is enabled.
*
* @since 4.2.1
*
* @return void
*/
protected function disableWpSitemap() {
if ( ! aioseo()->options->sitemap->general->enable ) {
return;
}
remove_action( 'init', 'wp_sitemaps_get_server' );
add_filter( 'wp_sitemaps_enabled', '__return_false' );
}
/**
* Check if the .htaccess rewrite rules are present if the user is using Apache. If not, add them.
*
* @since 4.2.5
*
* @return void
*/
private function maybeAddHtaccessRewriteRules() {
if ( ! aioseo()->helpers->isApache() || wp_doing_ajax() || wp_doing_cron() ) {
return;
}
ob_start();
aioseo()->templates->getTemplate( 'sitemap/htaccess-rewrite-rules.php' );
$rewriteRules = ob_get_clean();
$escapedRewriteRules = aioseo()->helpers->escapeRegex( $rewriteRules );
$contents = aioseo()->helpers->decodeHtmlEntities( aioseo()->htaccess->getContents() );
if ( get_option( 'permalink_structure' ) ) {
if ( preg_match( '/All in One SEO Sitemap Rewrite Rules/i', (string) $contents ) && ! aioseo()->core->cache->get( 'aioseo_sitemap_htaccess_rewrite_rules_remove' ) ) {
aioseo()->core->cache->update( 'aioseo_sitemap_htaccess_rewrite_rules_remove', time(), HOUR_IN_SECONDS );
$contents = preg_replace( "/$escapedRewriteRules/i", '', (string) $contents );
aioseo()->htaccess->saveContents( $contents );
}
return;
}
if ( preg_match( '/All in One SEO Sitemap Rewrite Rules/i', (string) $contents ) || aioseo()->core->cache->get( 'aioseo_sitemap_htaccess_rewrite_rules_add' ) ) {
return;
}
aioseo()->core->cache->update( 'aioseo_sitemap_htaccess_rewrite_rules_add', time(), HOUR_IN_SECONDS );
$contents .= $rewriteRules;
aioseo()->htaccess->saveContents( $contents );
}
/**
* Checks if static sitemap files prevent dynamic sitemap generation.
*
* @since 4.0.0
*
* @return void
*/
public function detectStatic() {
$isGeneralSitemapStatic = aioseo()->options->sitemap->general->advancedSettings->enable &&
in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) &&
! aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic;
if ( $isGeneralSitemapStatic ) {
Models\Notification::deleteNotificationByName( 'sitemap-static-files' );
return;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
$files = list_files( get_home_path(), 1 );
if ( ! count( $files ) ) {
return;
}
$detectedFiles = [];
if ( ! $isGeneralSitemapStatic ) {
foreach ( $files as $filename ) {
if ( preg_match( '#.*sitemap.*#', (string) $filename ) ) {
// We don't want to delete the video sitemap here at all.
$isVideoSitemap = preg_match( '#.*video.*#', (string) $filename ) ? true : false;
if ( ! $isVideoSitemap ) {
$detectedFiles[] = $filename;
}
}
}
}
$this->maybeShowStaticSitemapNotification( $detectedFiles );
}
/**
* If there are files, show a notice, otherwise delete it.
*
* @since 4.0.0
*
* @param array $detectedFiles An array of detected files.
* @return void
*/
protected function maybeShowStaticSitemapNotification( $detectedFiles ) {
if ( ! count( $detectedFiles ) ) {
Models\Notification::deleteNotificationByName( 'sitemap-static-files' );
return;
}
$notification = Models\Notification::getNotificationByName( 'sitemap-static-files' );
if ( $notification->notification_name ) {
return;
}
Models\Notification::addNotification( [
'slug' => uniqid(),
'notification_name' => 'sitemap-static-files',
'title' => __( 'Static sitemap files detected', 'all-in-one-seo-pack' ),
'content' => sprintf(
// Translators: 1 - The plugin short name ("AIOSEO"), 2 - Same as previous.
__( '%1$s has detected static sitemap files in the root folder of your WordPress installation.
As long as these files are present, %2$s is not able to dynamically generate your sitemap.', 'all-in-one-seo-pack' ),
AIOSEO_PLUGIN_SHORT_NAME,
AIOSEO_PLUGIN_SHORT_NAME
),
'type' => 'error',
'level' => [ 'all' ],
'button1_label' => __( 'Delete Static Files', 'all-in-one-seo-pack' ),
'button1_action' => 'http://action#sitemap/delete-static-files',
'start' => gmdate( 'Y-m-d H:i:s' )
] );
}
/**
* Regenerates the static sitemap files when a post is updated.
*
* @since 4.0.0
*
* @param integer $postId The post ID.
* @return void
*/
public function regenerateOnUpdate( $postId ) {
if ( aioseo()->helpers->isValidPost( $postId ) ) {
$this->scheduleRegeneration();
}
}
/**
* Schedules an action to regenerate the static sitemap files.
*
* @since 4.0.5
*
* @return void
*/
public function scheduleRegeneration() {
try {
if (
! aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic &&
! as_next_scheduled_action( 'aioseo_static_sitemap_regeneration' )
) {
as_schedule_single_action( time() + 60, 'aioseo_static_sitemap_regeneration', [], 'aioseo' );
}
} catch ( \Exception $e ) {
// Do nothing.
}
}
/**
* Regenerates the static sitemap files.
*
* @since 4.0.5
*
* @return void
*/
public function regenerateStaticSitemap() {
aioseo()->sitemap->file->generate();
}
/**
* Generates the requested sitemap.
*
* @since 4.0.0
*
* @return void
*/
public function generate() {
if ( empty( $this->type ) ) {
return;
}
// This is a hack to prevent WordPress from running it's default stuff during our processing.
global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
$wp_query->is_home = false; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
// This prevents the sitemap from including terms twice when WPML is active.
if ( class_exists( 'SitePress' ) ) {
global $sitepress_settings; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
// Before building the sitemap make sure links aren't translated.
// The setting should not be updated in the DB.
$sitepress_settings['auto_adjust_ids'] = 0; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
}
// If requested sitemap should be static and doesn't exist, then generate it.
// We'll then serve it dynamically for the current request so that we don't serve a blank page.
$this->doesFileExist();
$options = aioseo()->options->noConflict();
if ( ! $options->sitemap->{aioseo()->sitemap->type}->enable ) {
aioseo()->helpers->notFoundPage();
return;
}
$entries = aioseo()->sitemap->content->get();
$total = aioseo()->sitemap->content->getTotal();
if ( ! $entries ) {
$addonsEntries = aioseo()->addons->doAddonFunction( 'content', 'get' );
$addonTotals = aioseo()->addons->doAddonFunction( 'content', 'getTotal' );
foreach ( $addonsEntries as $addonSlug => $addonEntries ) {
if ( ! empty( $addonEntries ) ) {
$entries = $addonEntries;
$total = ! empty( $addonTotals[ $addonSlug ] ) ? $addonTotals[ $addonSlug ] : count( $entries );
break;
}
}
}
if ( 0 === $total && empty( $entries ) ) {
status_header( 404 );
}
$this->xsl->saveXslData(
aioseo()->sitemap->requestParser->slug,
$entries,
$total
);
$this->headers();
aioseo()->sitemap->output->output( $entries );
aioseo()->addons->doAddonFunction( 'output', 'output', [ $entries ] );
exit;
}
/**
* Checks if static file should be served and generates it if it doesn't exist.
*
* This essentially acts as a safety net in case a file doesn't exist yet or has been deleted.
*
* @since 4.0.0
*
* @return void
*/
protected function doesFileExist() {
aioseo()->addons->doAddonFunction( 'sitemap', 'doesFileExist' );
if (
'general' !== $this->type ||
! aioseo()->options->sitemap->general->advancedSettings->enable ||
! in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) ||
aioseo()->options->sitemap->general->advancedSettings->dynamic
) {
return;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
if ( isset( $_SERVER['REQUEST_URI'] ) && ! aioseo()->core->fs->exists( get_home_path() . sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ) {
$this->scheduleRegeneration();
}
}
/**
* Sets the HTTP headers for the sitemap.
*
* @since 4.0.0
*
* @return void
*/
public function headers() {
$charset = aioseo()->helpers->getCharset();
header( "Content-Type: text/xml; charset=$charset", true );
header( 'X-Robots-Tag: noindex, follow', true );
}
/**
* Registers an active sitemap addon and its classes.
* NOTE: This is deprecated and only there for users who already were using the previous sitemap addons version.
*
* @final 4.2.7
* @since 4.0.0
*
* @return void
*/
public function addAddon() {}
} Views/report/summary.php 0000666 00000164475 15113050717 0011407 0 ustar 00 <?php
/**
* Summary report view.
*
* @since 4.7.2
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
// phpcs:disable Generic.Files.LineLength.MaxExceeded
?>
<div style="background-color: #f3f4f5; color: #141b38; font-family: Helvetica, Roboto, Arial, sans-serif; font-size: 14px; line-height: 22px; margin: 0; padding: 0;">
<span style="display: none !important; visibility: hidden; opacity: 0; height: 0; width: 0;"><?php echo $preHeader ?? '' ?></span>
<div style="margin: 0 auto; padding: 70px 0; width: 100%; max-width: 680px;">
<div style="background-color: #ffffff; border: 1px solid #e8e8eb;">
<div style="padding-left: 20px; padding-right: 20px; padding-bottom: 20px;">
<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<thead>
<tr>
<th style="padding: 0; width: 60%; line-height: 1;"></th>
<th style="padding: 0; width: 40%; line-height: 1;"></th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 0;">
<div style="padding-top: 20px;">
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none;"
width="100"
height="20"
src="https://static.aioseo.io/report/ste/text-logo.jpg"
alt="<?php echo esc_attr( AIOSEO_PLUGIN_SHORT_NAME ) ?>"
/>
</div>
</td>
<td style="padding: 0; word-break: break-word;">
<div style="padding-top: 20px; font-size: 12px; text-align: right; line-height: 15px;"><?php echo $dateRange['range'] ?? ''; ?></div>
</td>
</tr>
<tr>
<td style="padding: 0; word-break: break-word;">
<div style="padding-top: 10px;">
<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;"><?php echo $heading ?? ''; ?></p>
</div>
</td>
<td style="padding: 0; word-break: break-word;">
<div style="padding-top: 10px; font-size: 12px; text-align: right; line-height: 15px;">
<a
href="<?php echo site_url(); ?>"
style="color: #005ae0; font-weight: normal; text-decoration: none;"
><?php echo site_url(); ?></a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div style="background-color: #004f9d; padding-bottom: 20px;">
<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<thead>
<tr>
<th style="padding: 0; width: 70%; line-height: 1;"></th>
<th style="padding: 0; width: 30%; line-height: 1;"></th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 0; word-break: break-word;">
<div style="padding-right: 20px; padding-left: 20px; padding-top: 20px; line-height: 1;">
<span style="color: #ffffff; margin-right: 3px; font-weight: 700; font-size: 28px; vertical-align: middle;"><?php esc_html_e( 'Hi there!', 'all-in-one-seo-pack' ); ?></span>
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
width="28"
height="28"
src="https://static.aioseo.io/report/ste/emoji-1f44b.png"
alt="Waving Hand Sign"
/>
</div>
<div style="color: #ffffff; padding-right: 20px; padding-left: 20px; padding-top: 20px; font-size: 20px; line-height: 26px; font-weight: 400;">
<?php echo $subheading ?? ''; ?>
</div>
</td>
<td style="padding: 0; text-align: right; word-break: break-word;">
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; padding-top: 20px;"
width="142"
height="140"
src="<?php echo esc_attr( 'https://static.aioseo.io/report/ste/' . ( $iconCalendar ?? '' ) . '.png' ) ?>"
alt=""
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<?php if ( aioseo()->siteHealth->shouldUpdate() ) { ?>
<div style="margin-top: 20px;">
<div style="text-align: center; font-size: 14px; border-radius: 4px; margin: 0; padding: 8px 12px; background-color: #fffbeb; border: 1px solid #f18200;">
<?php
printf(
// Translators: 1 - The plugin short name ("AIOSEO"), 2 - Opening link tag, 3 - HTML arrow, 4 - Closing link tag.
__( 'An update is available for %1$s. %2$sUpgrade to the latest version%3$s%4$s', 'all-in-one-seo-pack' ),
AIOSEO_PLUGIN_SHORT_NAME,
'<a href="' . ( $links['update'] ?? '#' ) . '" style="color: #005ae0; font-weight: normal; text-decoration: underline;">',
' →',
'</a>'
)
?>
</div>
</div>
<?php } ?>
<div style="background-color: #ffffff; border: 1px solid #e8e8eb; margin-top: 20px;">
<div style="border-bottom: 1px solid #e5e5e5; padding: 15px 20px;">
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 6px;"
width="35"
height="35"
src="https://static.aioseo.io/report/ste/icon-report.png"
alt=""
/>
<h2 style="font-size: 20px; font-weight: 700; line-height: 24px; margin-bottom: 0; margin-top: 0; vertical-align: middle; display: inline-block;"><?php esc_html_e( 'SEO Report', 'all-in-one-seo-pack' ); ?></h2>
</div>
<div style="padding: 20px;">
<?php if ( ! empty( $statisticsReport['posts']['winning'] ) ) { ?>
<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<tbody>
<tr>
<td style="padding: 0; word-break: break-word;">
<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;"><?php esc_html_e( 'Top Winning Posts', 'all-in-one-seo-pack' ); ?></p>
</td>
<td style="text-align: right; word-break: break-word;">
<?php if ( ! empty( $statisticsReport['posts']['winning']['url'] ) ) { ?>
<a
href="<?php echo esc_attr( $statisticsReport['posts']['winning']['url'] ) ?>"
style="color: #005ae0; font-weight: 700; text-decoration: underline;"
>
<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
</a>
<?php } ?>
</td>
</tr>
</tbody>
</table>
<div style="margin-top: 16px; overflow-x: auto;">
<table style="min-width: 460px; table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<thead>
<tr style="border-color: #ffffff; border-bottom-width: 6px; border-bottom-style: solid;">
<th style="width: 59%; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
<?php esc_html_e( 'Post', 'all-in-one-seo-pack' ); ?>
</th>
<th style="width: <?php echo $statisticsReport['posts']['winning']['show_tru_seo'] ? '17%' : '0' ?>; text-align: center; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
<?php if ( $statisticsReport['posts']['winning']['show_tru_seo'] ) { ?>
TruSEO
<?php } ?>
</th>
<th style="width: 12%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
<?php esc_html_e( 'Clicks', 'all-in-one-seo-pack' ); ?>
</th>
<th style="width: 12%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
<?php esc_html_e( 'Diff', 'all-in-one-seo-pack' ); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ( ( $statisticsReport['posts']['winning']['items'] ?? [] ) as $i => $item ) { ?>
<tr style="<?php echo 0 === $i % 2 ? 'background-color: #f3f4f5;' : '' ?>">
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; font-size: 14px;">
<a style="color: #141b38; font-weight: normal; text-decoration: none;"<?php echo ! empty( $item['url'] ) ? ' href="' . esc_attr( $item['url'] ) . '"' : '' ?>>
<?php echo $item['title']; ?>
</a>
</div>
</td>
<td style="padding: 0; word-break: break-word;">
<?php if ( ! empty( $item['tru_seo'] ) ) { ?>
<div style="padding: 6px;">
<div style="width: 45px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 12px; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['tru_seo']['color']}"; ?>">
<?php echo $item['tru_seo']['text']; ?>
</div>
</div>
<?php } ?>
</td>
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; font-size: 14px;">
<?php echo $item['clicks']; ?>
</div>
</td>
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; font-size: 0; <?php echo "color: {$item['difference']['clicks']['color']}"; ?>">
<?php if ( '#00aa63' === $item['difference']['clicks']['color'] ) { ?>
<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
<?php } ?>
<?php if ( '#df2a4a' === $item['difference']['clicks']['color'] ) { ?>
<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
<?php } ?>
<span style="display: inline-block; vertical-align: middle; font-size: 14px;"><?php echo $item['difference']['clicks']['text']; ?></span>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } ?>
<?php if ( ! empty( $statisticsReport['posts']['losing'] ) ) { ?>
<?php if ( ! empty( $statisticsReport['posts']['winning'] ) ) { ?>
<div style="margin-top: 20px; margin-bottom: 20px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
<?php } ?>
<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<tbody>
<tr>
<td style="padding: 0; word-break: break-word;">
<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;"><?php esc_html_e( 'Top Losing Posts', 'all-in-one-seo-pack' ); ?></p>
</td>
<td style="text-align: right; word-break: break-word;">
<?php if ( ! empty( $statisticsReport['posts']['losing']['url'] ) ) { ?>
<a
href="<?php echo esc_attr( $statisticsReport['posts']['losing']['url'] ) ?>"
style="color: #005ae0; font-weight: 700; text-decoration: underline;"
>
<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
</a>
<?php } ?>
</td>
</tr>
</tbody>
</table>
<div style="margin-top: 16px; overflow-x: auto;">
<table style="table-layout: fixed; min-width: 460px; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<thead>
<tr style="border-color: #ffffff; border-bottom-width: 6px; border-bottom-style: solid;">
<th style="width: 59%; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
<?php esc_html_e( 'Post', 'all-in-one-seo-pack' ); ?>
</th>
<th style="width: <?php echo $statisticsReport['posts']['losing']['show_tru_seo'] ? '17%' : '0' ?>; text-align: center; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
<?php if ( $statisticsReport['posts']['losing']['show_tru_seo'] ) { ?>
TruSEO
<?php } ?>
</th>
<th style="width: 12%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
<?php esc_html_e( 'Clicks', 'all-in-one-seo-pack' ); ?>
</th>
<th style="width: 12%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
<?php esc_html_e( 'Diff', 'all-in-one-seo-pack' ); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ( ( $statisticsReport['posts']['losing']['items'] ?? [] ) as $i => $item ) { ?>
<tr style="<?php echo 0 === $i % 2 ? 'background-color: #f3f4f5;' : '' ?>">
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; font-size: 14px;">
<a style="color: #141b38; font-weight: normal; text-decoration: none;"<?php echo ! empty( $item['url'] ) ? ' href="' . esc_attr( $item['url'] ) . '"' : '' ?>>
<?php echo $item['title']; ?>
</a>
</div>
</td>
<td style="padding: 0; word-break: break-word;">
<?php if ( ! empty( $item['tru_seo'] ) ) { ?>
<div style="padding: 6px;">
<div style="width: 45px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 12px; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['tru_seo']['color']}"; ?>">
<?php echo $item['tru_seo']['text']; ?>
</div>
</div>
<?php } ?>
</td>
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; font-size: 14px;">
<?php echo $item['clicks']; ?>
</div>
</td>
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; font-size: 0; <?php echo "color: {$item['difference']['clicks']['color']}"; ?>">
<?php if ( '#00aa63' === $item['difference']['clicks']['color'] ) { ?>
<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
<?php } ?>
<?php if ( '#df2a4a' === $item['difference']['clicks']['color'] ) { ?>
<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
<?php } ?>
<span style="display: inline-block; vertical-align: middle; font-size: 14px;"><?php echo $item['difference']['clicks']['text']; ?></span>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } ?>
<?php if ( ! empty( $statisticsReport['keywords']['winning'] ) || ! empty( $statisticsReport['keywords']['losing'] ) ) { ?>
<?php if ( ! empty( $statisticsReport['posts']['winning'] ) || ! empty( $statisticsReport['posts']['losing'] ) ) { ?>
<div style="margin-top: 20px; margin-bottom: 20px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
<?php } ?>
<div style="overflow-x: auto;">
<table style="min-width: 600px; table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<thead>
<tr>
<th style="width: 47%; line-height: 1;"></th>
<th style="width: 6%; line-height: 1;"></th>
<th style="width: 47%; line-height: 1;"></th>
</tr>
</thead>
<tbody>
<tr style="height: 1px;">
<td style="vertical-align: top; word-break: break-word;">
<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<tbody>
<tr>
<td style="padding: 0; word-break: break-word;">
<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;">
<?php esc_html_e( 'Top Winning Keywords', 'all-in-one-seo-pack' ); ?>
</p>
</td>
<td style="text-align: right; word-break: break-word;">
<?php if ( ! empty( $statisticsReport['keywords']['winning']['url'] ) ) { ?>
<a
href="<?php echo esc_attr( $statisticsReport['keywords']['winning']['url'] ) ?>"
style="color: #005ae0; font-weight: 700; text-decoration: underline;"
>
<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
</a>
<?php } ?>
</td>
</tr>
</tbody>
</table>
<table style="margin-top: 16px; table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<thead>
<tr style="border-color: #ffffff; border-bottom-width: 6px; border-bottom-style: solid;">
<th style="width: 64%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
<?php esc_html_e( 'Keyword', 'all-in-one-seo-pack' ); ?>
</th>
<th style="width: 16%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
<?php esc_html_e( 'Clicks', 'all-in-one-seo-pack' ); ?>
</th>
<th style="width: 20%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
<?php esc_html_e( 'Diff', 'all-in-one-seo-pack' ); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ( ( $statisticsReport['keywords']['winning']['items'] ?? [] ) as $i => $item ) { ?>
<tr style="<?php echo 0 === $i % 2 ? 'background-color: #f3f4f5;' : '' ?>">
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; font-size: 14px;">
<?php echo $item['title']; ?>
</div>
</td>
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; font-size: 14px;">
<?php echo $item['clicks']; ?>
</div>
</td>
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; font-size: 0; <?php echo "color: {$item['difference']['clicks']['color']}"; ?>">
<?php if ( '#00aa63' === $item['difference']['clicks']['color'] ) { ?>
<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
<?php } ?>
<?php if ( '#df2a4a' === $item['difference']['clicks']['color'] ) { ?>
<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
<?php } ?>
<span style="display: inline-block; vertical-align: middle; font-size: 14px;"><?php echo $item['difference']['clicks']['text']; ?></span>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</td>
<td style="height: inherit; padding: 0; text-align: center; vertical-align: baseline; overflow: hidden; word-break: break-word;">
<div style="width: 1px; margin-left: auto; margin-right: auto; background-color: #e5e5e5; height: 100%;"></div>
</td>
<td style="vertical-align: top; word-break: break-word;">
<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<tbody>
<tr>
<td style="padding: 0; word-break: break-word;">
<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;">
<?php esc_html_e( 'Top Losing Keywords', 'all-in-one-seo-pack' ); ?>
</p>
</td>
<td style="text-align: right; word-break: break-word;">
<?php if ( ! empty( $statisticsReport['keywords']['losing']['url'] ) ) { ?>
<a
href="<?php echo esc_attr( $statisticsReport['keywords']['losing']['url'] ) ?>"
style="color: #005ae0; font-weight: 700; text-decoration: underline;"
>
<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
</a>
<?php } ?>
</td>
</tr>
</tbody>
</table>
<table style="margin-top: 16px; table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<thead>
<tr style="border-color: #ffffff; border-bottom-width: 6px; border-bottom-style: solid;">
<th style="width: 64%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
<?php esc_html_e( 'Keyword', 'all-in-one-seo-pack' ); ?>
</th>
<th style="width: 16%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
<?php esc_html_e( 'Clicks', 'all-in-one-seo-pack' ); ?>
</th>
<th style="width: 20%; background-color: #f0f6ff; padding: 6px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
<?php esc_html_e( 'Diff', 'all-in-one-seo-pack' ); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ( ( $statisticsReport['keywords']['losing']['items'] ?? [] ) as $i => $item ) { ?>
<tr style="<?php echo 0 === $i % 2 ? 'background-color: #f3f4f5;' : '' ?>">
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; font-size: 14px;">
<?php echo $item['title']; ?>
</div>
</td>
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; font-size: 14px;">
<?php echo $item['clicks']; ?>
</div>
</td>
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; font-size: 0; <?php echo "color: {$item['difference']['clicks']['color']}"; ?>">
<?php if ( '#00aa63' === $item['difference']['clicks']['color'] ) { ?>
<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
<?php } ?>
<?php if ( '#df2a4a' === $item['difference']['clicks']['color'] ) { ?>
<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
<?php } ?>
<span style="display: inline-block; vertical-align: middle; font-size: 14px;"><?php echo $item['difference']['clicks']['text']; ?></span>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<?php } ?>
<?php if ( ! empty( $upsell['search-statistics'] ) ) { ?>
<div>
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
width="638"
height="146"
src="https://static.aioseo.io/report/ste/banner-search-statistics-cta-upsell.jpg"
alt=""
/>
<p style="font-size: 16px; margin-bottom: 0; margin-top: 20px; text-align: center;">
<?php esc_html_e( 'Connect your site to Google Search Console to receive insights on how content is being discovered. Identify areas for improvement and drive traffic to your website.', 'all-in-one-seo-pack' ); ?>
</p>
<div style="width: 475px; max-width: 96%; margin-top: 20px; margin-left: auto; margin-right: auto;">
<div style="width: 210px; padding: 6px; display: inline-block; vertical-align: middle;">
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 3px;"
width="17"
height="17"
src="https://static.aioseo.io/report/ste/icon-check-circle-out.png"
alt="✓"
/>
<span style="display: inline-block; vertical-align: middle; line-height: 20px; max-width: 185px;"><?php esc_html_e( 'Search traffic insights', 'all-in-one-seo-pack' ); ?></span>
</div>
<div style="width: 210px; padding: 6px; display: inline-block; vertical-align: middle;">
<img
style="margin-right: 3px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
width="17"
height="17"
src="https://static.aioseo.io/report/ste/icon-check-circle-out.png"
alt="✓"
/>
<span style="display: inline-block; vertical-align: middle; line-height: 20px; max-width: 185px;"><?php esc_html_e( 'Track page rankings', 'all-in-one-seo-pack' ); ?></span>
</div>
<div style="width: 210px; padding: 6px; display: inline-block; vertical-align: middle;">
<img
style="margin-right: 3px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
width="17"
height="17"
src="https://static.aioseo.io/report/ste/icon-check-circle-out.png"
alt="✓"
/>
<span style="display: inline-block; vertical-align: middle; line-height: 20px; max-width: 185px;"><?php esc_html_e( 'Track keyword rankings', 'all-in-one-seo-pack' ); ?></span>
</div>
<div style="width: 210px; padding: 6px; display: inline-block; vertical-align: middle;">
<img
style="margin-right: 3px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
width="17"
height="17"
src="https://static.aioseo.io/report/ste/icon-check-circle-out.png"
alt="✓"
/>
<span style="display: inline-block; vertical-align: middle; line-height: 20px; max-width: 185px;"><?php esc_html_e( 'Speed tests for individual pages/posts', 'all-in-one-seo-pack' ); ?></span>
</div>
</div>
<div style="margin-top: 20px; text-align: center;">
<a
href="<?php echo esc_attr( $upsell['search-statistics']['cta']['url'] ) ?>"
style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #00aa63; color: #ffffff; padding: 8px 20px;"
>
<?php echo $upsell['search-statistics']['cta']['text']; ?>
</a>
</div>
</div>
<?php } ?>
<?php if ( empty( $upsell['search-statistics'] ) && ! empty( $statisticsReport['cta'] ) ) { ?>
<div style="margin-top: 20px; margin-bottom: 20px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
<div style="text-align: center;">
<a
href="<?php echo esc_attr( $statisticsReport['cta']['url'] ) ?>"
style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #005ae0; color: #ffffff; padding: 8px 20px;"
>
<?php echo $statisticsReport['cta']['text']; ?>
</a>
</div>
<?php } ?>
</div>
</div>
<?php if ( ! empty( $posts ) ) { ?>
<div style="background-color: #ffffff; border: 1px solid #e8e8eb; margin-top: 20px;">
<div style="border-bottom: 1px solid #e5e5e5; padding: 15px 20px;">
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 6px;"
width="35"
height="35"
src="https://static.aioseo.io/report/ste/icon-summary.png"
alt=""
/>
<h2 style="font-size: 20px; font-weight: 700; line-height: 24px; margin-bottom: 0; margin-top: 0; vertical-align: middle; display: inline-block;"><?php esc_html_e( 'Content Summary', 'all-in-one-seo-pack' ); ?></h2>
</div>
<div style="padding: 20px;">
<?php if ( ! empty( $posts['publish']['items'] ) ) { ?>
<div>
<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<tbody>
<tr>
<td style="padding: 0; word-break: break-word;">
<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;">
<?php esc_html_e( 'Most Recent Published', 'all-in-one-seo-pack' ); ?>
</p>
</td>
<td style="text-align: right; word-break: break-word;">
<a
href="<?php echo esc_attr( $posts['publish']['url'] ) ?>"
style="color: #005ae0; font-weight: 700; text-decoration: underline;"
>
<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
</a>
</td>
</tr>
</tbody>
</table>
<div style="margin-top: 16px; overflow-x: auto;">
<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<thead>
<tr>
<th style="background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; border-top-left-radius: 2px; border-bottom-left-radius: 2px; width: 210px; line-height: 1;">
<?php esc_html_e( 'Post', 'all-in-one-seo-pack' ); ?>
</th>
<th style="text-align: center; background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; line-height: 1; width: <?php echo $posts['publish']['show_tru_seo'] ? '115px' : '0' ?>">
<?php if ( $posts['publish']['show_tru_seo'] ) { ?>
TruSEO
<?php } ?>
</th>
<th style="background-color: #f0f6ff; padding: 12px; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1; width: <?php echo $posts['publish']['show_stats'] ? '135px' : '0' ?>">
<?php if ( $posts['publish']['show_stats'] ) { ?>
<?php esc_html_e( 'Stats', 'all-in-one-seo-pack' ); ?>
<?php } ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ( $posts['publish']['items'] as $i => $item ) { ?>
<?php if ( $i > 0 ) { ?>
<tr>
<td
colspan="3"
style="padding: 0; word-break: break-word;"
>
<div style="border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
</td>
</tr>
<?php } ?>
<tr>
<td style="padding: 12px; word-break: break-word;">
<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<thead>
<tr>
<th style="width: 35%; padding: 0; line-height: 1;"></th>
<th style="width: 65%; padding: 0; line-height: 1;"></th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 0; word-break: break-word;">
<div style="width: 100%; height: 65px; position: relative; overflow: hidden;">
<a
style="color: #005ae0; font-weight: normal; text-decoration: underline;"
href="<?php echo esc_attr( $item['url'] ?: '#' ) ?>"
>
<img
src="<?php echo esc_attr( $item['image_url'] ); ?>"
alt="<?php echo esc_attr( aioseo()->helpers->stripEmoji( $item['title'] ) ); ?>"
style="box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; position: absolute; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; object-position: center; object-fit: cover; border-width: 1px; border-style: solid; border-color: #e5e5e5;"
/>
</a>
</div>
</td>
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; display: inline-block; vertical-align: middle; font-size: 14px;">
<a
style="color: #141b38; font-weight: normal; text-decoration: none;"
href="<?php echo esc_attr( $item['url'] ?: '#' ) ?>"
>
<?php echo $item['title']; ?>
</a>
</div>
</td>
</tr>
</tbody>
</table>
</td>
<td style="padding: 0; word-break: break-word;">
<?php if ( ! empty( $item['tru_seo'] ) ) { ?>
<div style="padding: 12px;">
<div style="width: 45px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 12px; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['tru_seo']['color']}"; ?>">
<?php echo $item['tru_seo']['text']; ?>
</div>
</div>
<?php } ?>
</td>
<td style="padding: 0; word-break: break-word;">
<?php if ( ! empty( $item['stats'] ) ) { ?>
<div style="padding: 12px; font-size: 14px;">
<?php foreach ( $item['stats'] as $k => $stat ) { ?>
<div style="line-height: 1;<?php echo $k > 0 ? ' margin-top: 6px;' : '' ?>">
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
width="19"
height="19"
src="<?php echo esc_attr( 'https://static.aioseo.io/report/ste/icon-' . $stat['icon'] . '.png' ) ?>"
alt=""
/>
<span style="vertical-align: middle;"><?php echo $stat['label']; ?>:</span>
<span style="vertical-align: middle; font-weight: 700;"><?php echo $stat['value']; ?></span>
</div>
<?php } ?>
</div>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
<?php } ?>
<?php if ( ! empty( $posts['optimize']['items'] ) ) { ?>
<?php if ( ! empty( $posts['publish']['items'] ) ) { ?>
<div style="margin-top: 30px; margin-bottom: 30px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
<?php } ?>
<div>
<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<tbody>
<tr>
<td style="padding: 0; word-break: break-word;">
<p style="font-size: 16px; margin-bottom: 0; margin-top: 0; font-weight: 700;">
<?php esc_html_e( 'Posts to Optimize', 'all-in-one-seo-pack' ); ?>
</p>
</td>
<td style="text-align: right; word-break: break-word;">
<a
href="<?php echo esc_attr( $posts['optimize']['url'] ) ?>"
style="color: #005ae0; font-weight: 700; text-decoration: underline;"
>
<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
</a>
</td>
</tr>
</tbody>
</table>
<div style="margin-top: 16px; overflow-x: auto;">
<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<thead>
<tr>
<th style="width: 319px; background-color: #f0f6ff; padding: 0; font-size: 12px; font-weight: 400; color: #434960; border-top-left-radius: 2px; border-bottom-left-radius: 2px; line-height: 1;">
<div style="padding: 12px;"><?php esc_html_e( 'Post', 'all-in-one-seo-pack' ); ?></div>
</th>
<th style="width: <?php echo $posts['optimize']['show_tru_seo'] ? '159px' : '0' ?>; text-align: center; background-color: #f0f6ff; padding: 0; font-size: 12px; font-weight: 400; color: #434960; line-height: 1;">
<?php if ( $posts['optimize']['show_tru_seo'] ) { ?>
<div style="padding: 12px;">TruSEO</div>
<?php } ?>
</th>
<th style="width: 159px; text-align: center; background-color: #f0f6ff; padding: 0; font-size: 12px; font-weight: 400; color: #434960; border-top-right-radius: 2px; border-bottom-right-radius: 2px; line-height: 1;">
<div style="padding: 12px;"><?php esc_html_e( 'Content Drop', 'all-in-one-seo-pack' ); ?></div>
</th>
</tr>
</thead>
<tbody>
<?php foreach ( $posts['optimize']['items'] as $item ) { ?>
<tr>
<td
colspan="3"
style="padding-bottom: 8px; padding-top: 8px; word-break: break-word;"
></td>
</tr>
<tr style="border-width: 1px; border-style: solid; border-color: #e5e5e5;">
<td
colspan="3"
style="padding: 12px; word-break: break-word;"
>
<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<thead>
<tr>
<th style="width: 311px; padding: 0; line-height: 1;"></th>
<th style="width: <?php echo $posts['optimize']['show_tru_seo'] ? '151px' : '0' ?>; padding: 0; line-height: 1;"></th>
<th style="width: 151px; padding: 0; line-height: 1;"></th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 0; background-color: #f3f4f5; word-break: break-word;">
<table style="table-layout: fixed; border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<thead>
<tr>
<th style="width: 35%; padding: 0; line-height: 1;"></th>
<th style="width: 65%; padding: 0; line-height: 1;"></th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 0; word-break: break-word;">
<div style="width: 100%; height: 65px; position: relative; overflow: hidden;">
<a style="color: #005ae0; font-weight: normal; text-decoration: underline;"<?php echo ! empty( $item['url'] ) ? ' href="' . esc_attr( $item['url'] ) . '"' : '' ?>>
<img
src="<?php echo esc_attr( $item['image_url'] ); ?>"
alt="<?php echo esc_attr( aioseo()->helpers->stripEmoji( $item['title'] ) ); ?>"
style="box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; position: absolute; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; object-position: center; object-fit: cover; border-width: 1px; border-style: solid; border-color: #e5e5e5;"
/>
</a>
</div>
</td>
<td style="padding: 0; word-break: break-word;">
<div style="padding: 6px; display: inline-block; vertical-align: middle; font-size: 14px;">
<a style="color: #141b38; font-weight: normal; text-decoration: none;"<?php echo ! empty( $item['url'] ) ? ' href="' . esc_attr( $item['url'] ) . '"' : '' ?>>
<?php echo $item['title']; ?>
</a>
</div>
</td>
</tr>
</tbody>
</table>
</td>
<td style="padding: 0; background-color: #f3f4f5; word-break: break-word;">
<?php if ( ! empty( $item['tru_seo'] ) ) { ?>
<div style="width: 45px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 12px; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['tru_seo']['color']}"; ?>">
<?php echo $item['tru_seo']['text']; ?>
</div>
<?php } ?>
</td>
<td style="padding: 0; background-color: #f3f4f5; word-break: break-word;">
<div style="width: 50px; padding: 6px; margin-left: auto; margin-right: auto; font-size: 0; text-align: center; line-height: 1; border-radius: 4px; border-width: 1px; border-style: solid; <?php echo "color: {$item['decay_percent']['color']}"; ?>">
<?php if ( '#00aa63' === $item['decay_percent']['color'] ) { ?>
<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-bottom-color: #00aa63; border-bottom-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-top-width: 0;"></div>
<?php } ?>
<?php if ( '#df2a4a' === $item['decay_percent']['color'] ) { ?>
<div style="display: inline-block; vertical-align: middle; margin-right: 3px; width: 0; border-style: solid; border-top-color: #df2a4a; border-top-width: 5px; border-left-color: transparent; border-right-color: transparent; border-left-width: 5px; border-right-width: 5px; border-bottom-width: 0;"></div>
<?php } ?>
<span style="display: inline-block; vertical-align: middle; font-size: 12px;"><?php echo $item['decay_percent']['text']; ?></span>
</div>
</td>
</tr>
<?php if ( ! empty( $item['issues']['items'] ) ) { ?>
<tr>
<td
colspan="2"
style="padding: 10px 0 0; word-break: break-word;"
>
<div style="font-size: 14px; font-weight: 700;"><?php esc_html_e( 'Issues', 'all-in-one-seo-pack' ); ?></div>
</td>
<td style="padding: 10px 0 0; text-align: right; word-break: break-word;">
<?php if ( ! empty( $item['issues']['url'] ) ) { ?>
<a
href="<?php echo esc_attr( $item['issues']['url'] ) ?>"
style="color: #005ae0; font-weight: normal; text-decoration: underline; font-size: 12px;"
>
<?php esc_html_e( 'View All', 'all-in-one-seo-pack' ); ?>
</a>
<?php } ?>
</td>
</tr>
<tr>
<td
colspan="3"
style="padding: 0; word-break: break-word;"
>
<?php foreach ( $item['issues']['items'] as $issue ) { ?>
<div style="padding-top: 0; padding-left: 0; padding-right: 0; pt-6 font-size: 14px;">
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 3px;"
width="15"
height="15"
src="https://static.aioseo.io/report/ste/icon-remove.png"
alt="x"
/>
<span style="width: 94%; vertical-align: middle; display: inline-block;"><?php echo $issue['text'] ?></span>
</div>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
<?php } ?>
<?php if ( empty( $posts['publish']['items'] ) && empty( $posts['optimize']['items'] ) ) { ?>
<div style="font-size: 16px; font-weight: 400; text-align: center;">
<?php echo esc_html__( 'It seems there is no content yet to be displayed.', 'all-in-one-seo-pack' ) ?>
</div>
<?php } ?>
<div style="margin-top: 20px; text-align: center;">
<a
href="<?php echo esc_attr( $posts['cta']['url'] ); ?>"
style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #005ae0; color: #ffffff; padding: 8px 20px;"
>
<?php echo $posts['cta']['text']; ?>
</a>
</div>
</div>
</div>
<?php } ?>
<?php if ( ! empty( $statisticsReport['milestones'] ) ) { ?>
<div style="background-color: #ffffff; border: 1px solid #e8e8eb; margin-top: 20px;">
<div style="border-bottom: 1px solid #e5e5e5; padding: 15px 20px;">
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 6px;"
width="35"
height="35"
src="https://static.aioseo.io/report/ste/icon-flag.png"
alt=""
/>
<h2 style="font-size: 20px; font-weight: 700; line-height: 24px; margin-bottom: 0; margin-top: 0; vertical-align: middle; display: inline-block;"><?php esc_html_e( 'SEO Milestones', 'all-in-one-seo-pack' ); ?></h2>
</div>
<div style="padding: 10px; overflow-x: auto;">
<table style="min-width: 400px; width: 100%;">
<tbody>
<?php for ( $i = 0; $i < count( $statisticsReport['milestones'] ) / 2; $i++ ) { ?>
<tr>
<?php for ( $j = $i; $j < ( 2 + $i ); $j++ ) { ?>
<?php $milestone = $statisticsReport['milestones'][ $i + $j ] ?? null ?>
<?php if ( ! $milestone ) { ?>
<?php continue; ?>
<?php } ?>
<td style="padding: 16px; word-break: break-word; vertical-align: top; border: 10px solid #ffff; text-align: center; border-radius: 4px; background-color: <?php echo $milestone['background'] ?>; color: <?php echo $milestone['color'] ?>; width: <?php echo max( 50, 100 / count( $statisticsReport['milestones'] ) ), '%' ?>">
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-left: auto; margin-right: auto;"
width="35"
height="35"
src="<?php echo 'https://static.aioseo.io/report/ste/', $milestone['icon'], '.png' ?>"
alt=""
/>
<p style="font-size: 16px; margin-bottom: 0; margin-top: 12px;">
<?php echo $milestone['message']; ?>
</p>
</td>
<?php } ?>
</tr>
<?php } ?>
</tbody>
</table>
<?php if ( ! empty( $statisticsReport['cta'] ) ) { ?>
<div style="margin: 10px 10px 20px; border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
<div style="padding-bottom: 10px; text-align: center;">
<a
href="<?php echo esc_attr( $statisticsReport['cta']['url'] ) ?>"
style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #005ae0; color: #ffffff; padding: 8px 20px;"
>
<?php echo $statisticsReport['cta']['text']; ?>
</a>
</div>
<?php } ?>
</div>
</div>
<?php } ?>
<?php if ( ! empty( $resources['posts'] ) ) { ?>
<div style="background-color: #ffffff; border: 1px solid #e8e8eb; margin-top: 20px;">
<div style="border-bottom: 1px solid #e5e5e5; padding: 15px 20px;">
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-right: 6px;"
width="35"
height="35"
src="https://static.aioseo.io/report/ste/icon-star.png"
alt=""
/>
<h2 style="font-size: 20px; font-weight: 700; line-height: 24px; margin-bottom: 0; margin-top: 0; vertical-align: middle; display: inline-block;"><?php esc_html_e( "What's New", 'all-in-one-seo-pack' ); ?></h2>
</div>
<div style="padding: 20px;">
<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<tbody>
<?php foreach ( $resources['posts'] as $k => $post ) { ?>
<?php if ( $k > 0 ) { ?>
<tr>
<td
colspan="3"
style="padding-bottom: 8px; padding-top: 8px; word-break: break-word;"
>
<div style="border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
</td>
</tr>
<?php } ?>
<tr>
<td style="padding: 0; word-break: break-word;">
<div style="width: 147px; height: 82px; margin-top: 6px; margin-bottom: 6px; margin-right: 6px; display: inline-block; vertical-align: top; position: relative; overflow: hidden;">
<a
style="color: #005ae0; font-weight: normal; text-decoration: underline;"
href="<?php echo esc_attr( $post['url'] ?: '#' ); ?>"
>
<img
src="<?php echo esc_attr( $post['image']['url'] ) ?>"
alt="<?php echo esc_attr( aioseo()->helpers->stripEmoji( $post['title'] ) ) ?>"
style="box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; position: absolute; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; object-position: center; object-fit: cover; border-width: 1px; border-style: solid; border-color: #e5e5e5;"
/>
</a>
</div>
<div style="max-width: 448px; padding-bottom: 3px; padding-top: 3px; display: inline-block; vertical-align: top;">
<a
style="color: #141b38; font-weight: 700; text-decoration: none; font-size: 16px;"
href="<?php echo esc_attr( $post['url'] ?: '#' ); ?>"
>
<?php echo $post['title']; ?>
</a>
<div style="margin-top: 6px; font-size: 14px;">
<span style="display: block;"><?php echo $post['content'] ?? ''; ?></span>
<a
style="color: #005ae0; font-weight: normal; text-decoration: underline;"
href="<?php echo esc_attr( $post['url'] ?: '#' ); ?>"
>
<?php esc_html_e( 'Continue Reading', 'all-in-one-seo-pack' ) ?>
</a>
</div>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<div style="padding-top: 8px;">
<div style="border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
</div>
<div style="padding-top: 20px; text-align: center;">
<a
href="<?php echo esc_attr( $resources['cta']['url'] ); ?>"
style="border-radius: 4px; border: none; display: inline-block; font-size: 14px; font-style: normal; font-weight: 700; text-align: center; text-decoration: none; user-select: none; vertical-align: middle; background-color: #005ae0; color: #ffffff; padding: 8px 20px;"
>
<?php echo $resources['cta']['text']; ?>
</a>
</div>
</div>
</div>
<?php } ?>
<div style="width: 600px; max-width: 90%; margin-top: 20px; margin-left: auto; margin-right: auto;">
<div style="text-align: center;">
<img
style="border-radius: 9999px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; margin-left: auto; margin-right: auto;"
src="https://static.aioseo.io/report/ste/danny-circle.jpg"
width="50"
height="50"
alt="<?php echo esc_attr( AIOSEO_PLUGIN_SHORT_NAME ) ?>"
/>
<p style="font-size: 14px; margin-bottom: 0; margin-top: 20px; text-align: center; color: #434960;">
<?php
// Translators: 1 - The plugin short name ("AIOSEO").
printf( esc_html__( 'This email was auto-generated and sent from %1$s.', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME )
?>
</p>
<p style="font-size: 14px; margin-bottom: 0; margin-top: 0; text-align: center; color: #434960;">
<?php
printf(
// Translators: 1 - Opening link tag, 2 - Closing link tag.
esc_html__( 'Learn how to %1$sdisable%2$s it.', 'all-in-one-seo-pack' ),
'<a href="' . ( $links['disable'] ?? '#' ) . '" style="color: #141b38; font-weight: normal; text-decoration: underline;">', '</a>'
)
?>
</p>
</div>
<div style="margin-top: 20px;">
<div style="border-top-width: 0; border-bottom-width: 1px; border-style: solid; border-color: #e5e5e5;"></div>
</div>
<div style="margin-top: 20px;">
<table style="border-collapse: collapse; text-align: left; vertical-align: middle; width: 100%;">
<tbody>
<tr>
<td style="line-height: 1; word-break: break-word;">
<a
style="color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
href="<?php echo esc_attr( $links['marketing-site'] ?? '#' ) ?>"
>
<img
style="border: none; box-sizing: border-box; display: inline-block; font-size: 14px; height: auto; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle;"
width="82"
height="17"
src="https://static.aioseo.io/report/ste/text-logo.png"
alt="<?php echo esc_attr( AIOSEO_PLUGIN_SHORT_NAME ) ?>"
/>
</a>
</td>
<td style="line-height: 1; text-align: right; word-break: break-word;">
<a
style="margin-right: 6px; color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
href="<?php echo esc_attr( $links['facebook'] ?? '#' ) ?>"
>
<img
style="border-radius: 2px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; height: 20px; width: 20px;"
src="https://static.aioseo.io/report/ste/facebook.jpg"
alt="Fb"
width="20"
height="20"
/>
</a>
<a
style="margin-right: 6px; color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
href="<?php echo esc_attr( $links['linkedin'] ?? '#' ) ?>"
>
<img
style="border-radius: 2px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; height: 20px; width: 20px;"
src="https://static.aioseo.io/report/ste/linkedin.jpg"
alt="In"
width="20"
height="20"
/>
</a>
<a
style="margin-right: 6px; color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
href="<?php echo esc_attr( $links['youtube'] ?? '#' ) ?>"
>
<img
style="border-radius: 2px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; height: 20px; width: 20px;"
src="https://static.aioseo.io/report/ste/youtube.jpg"
alt="Yt"
width="20"
height="20"
/>
</a>
<a
style="color: #005ae0; font-weight: normal; text-decoration: none; display: inline-block;"
href="<?php echo esc_attr( $links['twitter'] ?? '#' ) ?>"
>
<img
style="border-radius: 2px; border: none; box-sizing: border-box; display: inline-block; font-size: 14px; line-height: 1; max-width: 100%; text-decoration: none; vertical-align: middle; height: 20px; width: 20px;"
src="https://static.aioseo.io/report/ste/x.jpg"
alt="Tw"
width="20"
height="20"
/>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>