���� JFIF �� �
"" $(4,$&1'-=-157:::#+?D?8C49:7
7%%77777777777777777777777777777777777777777777777777�� { �" �� �� 5 !1AQa"q�2��BR��#b������� �� �� ? ��D@DDD@DDD@DDkK��6 �UG�4V�1��
�����릟�@�#���RY�dqp�
����� �o�7�m�s�<��VPS�e~V�چ8���X�T��$��c�� 9��ᘆ�m6@ WU�f�Don��r��5}9��}��hc�fF��/r=hi�� �͇�*�� b�.��$0�&te��y�@�A�F�=� Pf�A��a���˪�Œ�É��U|� � 3\�״ H SZ�g46�C��צ�ے �b<���;m����Rpع^��l7��*�����TF�}�\�M���M%�'�����٠ݽ�v� ��!-�����?�N!La��A+[`#���M����'�~oR�?��v^)��=��h����A��X�.���˃����^Æ��ܯsO"B�c>;
�e�4��5�k��/CB��.
�J?��;�҈�������������������~�<�VZ�ê¼2/)Í”jC���ע�V�G�!���!�F������\�� Kj�R�oc�h���:Þ I��1"2�q×°8��Р@ז���_C0�ր��A��lQ��@纼�!7��F�� �]�sZ
B�62r�v�z~�K�7�c��5�.���ӄq&�Z�d�<�kk���T&8�|���I���� Ws}���ǽ�cqnΑ�_���3��|N�-y,��i���ȗ_�\60���@��6����D@DDD@DDD@DDD@DDD@DDc�KN66<�c��64=r�����
Ď0��h���t&(�hnb[� ?��^��\��â|�,�/h�\��R��5�?
�0�!צ܉-����G����٬��Q�zA���1�����V��� �:R���`�$��ik��H����D4�����#dk����� h�}����7���w%�������*o8wG�LycuT�.���ܯ7��I��u^���)��/c�,s�Nq�ۺ�;�ך�YH2���.5B���DDD@DDD@DDD@DDD@DDD@V|�a�j{7c��X�F\�3MuA׾hb� ��n��F������ ��8�(��e����Pp�\"G�`s��m��ާaW�K��O����|;ei����֋�[�q��";a��1����Y�G�W/�߇�&�<���Ќ�H'q�m���)�X+!���=�m�ۚ丷~6a^X�)���,�>#&6G���Y��{����"" """ """ """ """ ""��at\/�a�8 �yp%�lhl�n����)���i�t��B�������������?��
tag. */
esc_html__( '%1$s Now with 30%% Black Friday Discount!', 'wordpress-seo' ),
'
'
);
}
echo '
';
echo ' '
. sprintf(
/* translators: %1$s is the plugin name, %2$s and %3$s are a link. */
esc_html__( '%1$s can\'t be updated because your product subscription is expired. %2$sRenew your product subscription%3$s to get updates again and use all the features of %1$s.', 'wordpress-seo' ),
esc_html( $plugin_data['name'] ),
'',
''
)
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped above.
. $sale_copy
. '';
}
}
/**
* Checks if there are any installed addons.
*
* @return bool True when there are installed Yoast addons.
*/
public function has_installed_addons() {
$installed_addons = $this->get_installed_addons();
return ! empty( $installed_addons );
}
/**
* Checks if the plugin is installed and activated in WordPress.
*
* @param string $slug The class' slug.
*
* @return bool True when installed and activated.
*/
public function is_installed( $slug ) {
$slug_to_class_map = [
static::PREMIUM_SLUG => 'WPSEO_Premium',
static::NEWS_SLUG => 'WPSEO_News',
static::WOOCOMMERCE_SLUG => 'Yoast_WooCommerce_SEO',
static::VIDEO_SLUG => 'WPSEO_Video_Sitemap',
static::LOCAL_SLUG => 'WPSEO_Local_Core',
];
if ( ! isset( $slug_to_class_map[ $slug ] ) ) {
return false;
}
return class_exists( $slug_to_class_map[ $slug ] );
}
/**
* Validates the addons and show a notice for the ones that are invalid.
*
* @return void
*/
public function validate_addons() {
$notification_center = Yoast_Notification_Center::get();
if ( $notification_center === null ) {
return;
}
foreach ( $this->addon_details as $slug => $addon_info ) {
$notification = $this->create_notification( $addon_info['name'], $addon_info['short_link_activation'] );
// Add a notification when the installed plugin isn't activated in My Yoast.
if ( $this->is_installed( $slug ) && ! $this->has_valid_subscription( $slug ) ) {
$notification_center->add_notification( $notification );
continue;
}
$notification_center->remove_notification( $notification );
}
}
/**
* Removes the site information transients.
*
* @codeCoverageIgnore
*
* @return void
*/
public function remove_site_information_transients() {
delete_transient( self::SITE_INFORMATION_TRANSIENT );
delete_transient( self::SITE_INFORMATION_TRANSIENT_QUICK );
}
/**
* Creates an instance of Yoast_Notification.
*
* @param string $product_name The product to create the notification for.
* @param string $short_link The short link for the addon notification.
*
* @return Yoast_Notification The created notification.
*/
protected function create_notification( $product_name, $short_link ) {
$notification_options = [
'type' => Yoast_Notification::ERROR,
'id' => 'wpseo-dismiss-' . sanitize_title_with_dashes( $product_name, null, 'save' ),
'capabilities' => 'wpseo_manage_options',
];
return new Yoast_Notification(
sprintf(
/* translators: %1$s expands to a strong tag, %2$s expands to the product name, %3$s expands to a closing strong tag, %4$s expands to an a tag. %5$s expands to MyYoast, %6$s expands to a closing a tag, %7$s expands to the product name */
__( '%1$s %2$s isn\'t working as expected %3$s and you are not receiving updates or support! Make sure to %4$s activate your product subscription in %5$s%6$s to unlock all the features of %7$s.', 'wordpress-seo' ),
'',
$product_name,
'',
'',
'MyYoast',
'',
$product_name
),
$notification_options
);
}
/**
* Checks whether a plugin expiry date has been passed.
*
* @param stdClass $subscription Plugin subscription.
*
* @return bool Has the plugin expired.
*/
protected function has_subscription_expired( $subscription ) {
return ( strtotime( $subscription->expiry_date ) - time() ) < 0;
}
/**
* Converts a subscription to plugin based format.
*
* @param stdClass $subscription The subscription to convert.
* @param stdClass|null $yoast_free_data The Yoast Free's data.
* @param bool $plugin_info Whether we're in the plugin information modal.
* @param string $plugin_file The plugin filename.
*
* @return stdClass The converted subscription.
*/
protected function convert_subscription_to_plugin( $subscription, $yoast_free_data = null, $plugin_info = false, $plugin_file = '' ) {
$changelog = '';
if ( isset( $subscription->product->changelog ) ) {
// We need to replace h2's and h3's with h4's because the styling expects that.
$changelog = str_replace( 'product->changelog ) );
$changelog = str_replace( ' ( $plugin_info ) ? YOAST_SEO_WP_REQUIRED : null,
];
return (object) [
'new_version' => ( $subscription->product->version ?? '' ),
'name' => $subscription->product->name,
'slug' => $subscription->product->slug,
'plugin' => $plugin_file,
'url' => $subscription->product->store_url,
'last_update' => $subscription->product->last_updated,
'homepage' => $subscription->product->store_url,
'download_link' => $subscription->product->download,
'package' => $subscription->product->download,
'sections' => [
'changelog' => $changelog,
'support' => $this->get_support_section(),
],
'icons' => [
'2x' => $this->get_icon( $subscription->product->slug ),
],
'update_supported' => true,
'banners' => $this->get_banners( $subscription->product->slug ),
// If we have extracted Yoast Free's data before, use that. If not, resort to the defaults.
'tested' => YOAST_SEO_WP_TESTED,
'requires' => ( $yoast_free_data->requires ?? $defaults['requires'] ),
'requires_php' => YOAST_SEO_PHP_REQUIRED,
];
}
/**
* Returns the plugin's icon URL.
*
* @param string $slug The plugin slug.
*
* @return string The icon URL for this plugin.
*/
protected function get_icon( $slug ) {
switch ( $slug ) {
case self::LOCAL_SLUG:
return 'https://yoa.st/local-seo-icon';
case self::NEWS_SLUG:
return 'https://yoa.st/news-seo-icon';
case self::PREMIUM_SLUG:
return 'https://yoa.st/yoast-seo-icon';
case self::VIDEO_SLUG:
return 'https://yoa.st/video-seo-icon';
case self::WOOCOMMERCE_SLUG:
return 'https://yoa.st/woo-seo-icon';
}
}
/**
* Return an array of plugin banner URLs.
*
* @param string $slug The plugin slug.
*
* @return string[]
*/
protected function get_banners( $slug ) {
switch ( $slug ) {
case self::LOCAL_SLUG:
return [
'high' => 'https://yoa.st/yoast-seo-banner-local',
'low' => 'https://yoa.st/yoast-seo-banner-low-local',
];
case self::NEWS_SLUG:
return [
'high' => 'https://yoa.st/yoast-seo-banner-news',
'low' => 'https://yoa.st/yoast-seo-banner-low-news',
];
case self::PREMIUM_SLUG:
return [
'high' => 'https://yoa.st/yoast-seo-banner-premium',
'low' => 'https://yoa.st/yoast-seo-banner-low-premium',
];
case self::VIDEO_SLUG:
return [
'high' => 'https://yoa.st/yoast-seo-banner-video',
'low' => 'https://yoa.st/yoast-seo-banner-low-video',
];
case self::WOOCOMMERCE_SLUG:
return [
'high' => 'https://yoa.st/yoast-seo-banner-woo',
'low' => 'https://yoa.st/yoast-seo-banner-low-woo',
];
}
}
/**
* Checks if the given plugin_file belongs to a Yoast addon.
*
* @param string $plugin_file Path to the plugin.
*
* @return bool True when plugin file is for a Yoast addon.
*/
protected function is_yoast_addon( $plugin_file ) {
return $this->get_slug_by_plugin_file( $plugin_file ) !== '';
}
/**
* Retrieves the addon slug by given plugin file path.
*
* @param string $plugin_file The file path to the plugin.
*
* @return string The slug when found or empty string when not.
*/
protected function get_slug_by_plugin_file( $plugin_file ) {
$addons = self::$addons;
// Yoast SEO Free isn't an addon, but we needed it in Premium to fetch translations.
if ( YoastSEO()->helpers->product->is_premium() ) {
$addons['wp-seo.php'] = self::FREE_SLUG;
}
foreach ( $addons as $addon => $addon_slug ) {
if ( strpos( $plugin_file, $addon ) !== false ) {
return $addon_slug;
}
}
return '';
}
/**
* Retrieves the installed Yoast addons.
*
* @return array The installed plugins.
*/
protected function get_installed_addons() {
return array_filter( $this->get_plugins(), [ $this, 'is_yoast_addon' ], ARRAY_FILTER_USE_KEY );
}
/**
* Retrieves a list of active addons.
*
* @return array The active addons.
*/
protected function get_active_addons() {
return array_filter( $this->get_installed_addons(), [ $this, 'is_plugin_active' ], ARRAY_FILTER_USE_KEY );
}
/**
* Retrieves the current sites from the API.
*
* @codeCoverageIgnore
*
* @return bool|stdClass Object when request is successful. False if not.
*/
protected function request_current_sites() {
$api_request = new WPSEO_MyYoast_Api_Request( 'sites/current' );
if ( $api_request->fire() ) {
return $api_request->get_response();
}
return $this->get_site_information_default();
}
/**
* Retrieves the transient value with the site information.
*
* @codeCoverageIgnore
*
* @return stdClass|false The transient value.
*/
protected function get_site_information_transient() {
global $pagenow;
// Force re-check on license & dashboard pages.
$current_page = null;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
if ( isset( $_GET['page'] ) && is_string( $_GET['page'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing and thus no need to sanitize.
$current_page = wp_unslash( $_GET['page'] );
}
// Check whether the licenses are valid or whether we need to show notifications.
$quick = ( $current_page === 'wpseo_licenses' || $current_page === 'wpseo_dashboard' );
// Also do a fresh request on Plugins & Core Update pages.
$quick = $quick || $pagenow === 'plugins.php';
$quick = $quick || $pagenow === 'update-core.php';
if ( $quick ) {
return get_transient( self::SITE_INFORMATION_TRANSIENT_QUICK );
}
return get_transient( self::SITE_INFORMATION_TRANSIENT );
}
/**
* Sets the site information transient.
*
* @codeCoverageIgnore
*
* @param stdClass $site_information The site information to save.
*
* @return void
*/
protected function set_site_information_transient( $site_information ) {
set_transient( self::SITE_INFORMATION_TRANSIENT, $site_information, DAY_IN_SECONDS );
set_transient( self::SITE_INFORMATION_TRANSIENT_QUICK, $site_information, 60 );
}
/**
* Retrieves all installed WordPress plugins.
*
* @codeCoverageIgnore
*
* @return array The plugins.
*/
protected function get_plugins() {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return get_plugins();
}
/**
* Checks if the given plugin file belongs to an active plugin.
*
* @codeCoverageIgnore
*
* @param string $plugin_file The file path to the plugin.
*
* @return bool True when plugin is active.
*/
protected function is_plugin_active( $plugin_file ) {
return is_plugin_active( $plugin_file );
}
/**
* Returns an object with no subscriptions.
*
* @codeCoverageIgnore
*
* @return stdClass Site information.
*/
protected function get_site_information_default() {
return (object) [
'url' => WPSEO_Utils::get_home_url(),
'subscriptions' => [],
];
}
/**
* Maps the plugin API response.
*
* @param object $site_information Site information as received from the API.
*
* @return stdClass Mapped site information.
*/
protected function map_site_information( $site_information ) {
return (object) [
'url' => $site_information->url,
'subscriptions' => array_map( [ $this, 'map_subscription' ], $site_information->subscriptions ),
];
}
/**
* Maps a plugin subscription.
*
* @param object $subscription Subscription information as received from the API.
*
* @return stdClass Mapped subscription.
*/
protected function map_subscription( $subscription ) {
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Not our properties.
return (object) [
'renewal_url' => $subscription->renewalUrl,
'expiry_date' => $subscription->expiryDate,
'product' => (object) [
'version' => $subscription->product->version,
'name' => $subscription->product->name,
'slug' => $subscription->product->slug,
'last_updated' => $subscription->product->lastUpdated,
'store_url' => $subscription->product->storeUrl,
// Ternary operator is necessary because download can be undefined.
'download' => ( $subscription->product->download ?? null ),
'changelog' => $subscription->product->changelog,
],
];
// phpcs:enable
}
/**
* Retrieves the site information.
*
* @return stdClass The site information.
*/
private function get_site_information() {
if ( ! $this->has_installed_addons() ) {
return $this->get_site_information_default();
}
return $this->get_myyoast_site_information();
}
/**
* Retrieves the contents for the support section.
*
* @return string The support section content.
*/
protected function get_support_section() {
return '' . __( 'Need support?', 'wordpress-seo' ) . '
'
. '
' /* translators: 1: expands to that refers to the help page, 2: closing tag. */ . sprintf( __( 'You can probably find an answer to your question in our %1$shelp center%2$s.', 'wordpress-seo' ), '', '' ) . ' ' /* translators: %s expands to a mailto support link. */ . sprintf( __( 'If you still need support and have an active subscription for this product, please email %s.', 'wordpress-seo' ), 'support@yoast.com' ) . '
'; } }