657 lines
21 KiB
PHP
657 lines
21 KiB
PHP
<?php
|
|
/**
|
|
* WPSEO plugin file.
|
|
*
|
|
* @package WPSEO\Internals\Options
|
|
*/
|
|
|
|
/**
|
|
* Option: wpseo.
|
|
*/
|
|
class WPSEO_Option_Wpseo extends WPSEO_Option {
|
|
|
|
/**
|
|
* Option name.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $option_name = 'wpseo';
|
|
|
|
/**
|
|
* Array of defaults for the option.
|
|
*
|
|
* {@internal Shouldn't be requested directly, use $this->get_defaults();}}
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $defaults = [
|
|
// Non-form fields, set via (ajax) function.
|
|
'tracking' => null,
|
|
'toggled_tracking' => false,
|
|
'license_server_version' => false,
|
|
'ms_defaults_set' => false,
|
|
'ignore_search_engines_discouraged_notice' => false,
|
|
'indexing_first_time' => true,
|
|
'indexing_started' => null,
|
|
'indexing_reason' => '',
|
|
'indexables_indexing_completed' => false,
|
|
'index_now_key' => '',
|
|
// Non-form field, should only be set via validation routine.
|
|
'version' => '', // Leave default as empty to ensure activation/upgrade works.
|
|
'previous_version' => '',
|
|
// Form fields.
|
|
'disableadvanced_meta' => true,
|
|
'enable_headless_rest_endpoints' => true,
|
|
'ryte_indexability' => false,
|
|
'baiduverify' => '', // Text field.
|
|
'googleverify' => '', // Text field.
|
|
'msverify' => '', // Text field.
|
|
'yandexverify' => '',
|
|
'site_type' => '', // List of options.
|
|
'has_multiple_authors' => '',
|
|
'environment_type' => '',
|
|
'content_analysis_active' => true,
|
|
'keyword_analysis_active' => true,
|
|
'inclusive_language_analysis_active' => false,
|
|
'enable_admin_bar_menu' => true,
|
|
'enable_cornerstone_content' => true,
|
|
'enable_xml_sitemap' => true,
|
|
'enable_text_link_counter' => true,
|
|
'enable_index_now' => true,
|
|
'enable_ai_generator' => false,
|
|
'show_onboarding_notice' => false,
|
|
'first_activated_on' => false,
|
|
'myyoast-oauth' => [
|
|
'config' => [
|
|
'clientId' => null,
|
|
'secret' => null,
|
|
],
|
|
'access_tokens' => [],
|
|
],
|
|
'semrush_integration_active' => true,
|
|
'semrush_tokens' => [],
|
|
'semrush_country_code' => 'us',
|
|
'permalink_structure' => '',
|
|
'home_url' => '',
|
|
'dynamic_permalinks' => false,
|
|
'category_base_url' => '',
|
|
'tag_base_url' => '',
|
|
'custom_taxonomy_slugs' => [],
|
|
'enable_enhanced_slack_sharing' => true,
|
|
'zapier_integration_active' => false,
|
|
'zapier_subscription' => [],
|
|
'zapier_api_key' => '',
|
|
'enable_metabox_insights' => true,
|
|
'enable_link_suggestions' => true,
|
|
'algolia_integration_active' => false,
|
|
'import_cursors' => [],
|
|
'workouts_data' => [ 'configuration' => [ 'finishedSteps' => [] ] ],
|
|
'configuration_finished_steps' => [],
|
|
'dismiss_configuration_workout_notice' => false,
|
|
'dismiss_premium_deactivated_notice' => false,
|
|
'importing_completed' => [],
|
|
'wincher_integration_active' => true,
|
|
'wincher_tokens' => [],
|
|
'wincher_automatically_add_keyphrases' => false,
|
|
'wincher_website_id' => '',
|
|
'wordproof_integration_active' => false,
|
|
'wordproof_integration_changed' => false,
|
|
'first_time_install' => false,
|
|
'should_redirect_after_install_free' => false,
|
|
'activation_redirect_timestamp_free' => 0,
|
|
'remove_feed_global' => false,
|
|
'remove_feed_global_comments' => false,
|
|
'remove_feed_post_comments' => false,
|
|
'remove_feed_authors' => false,
|
|
'remove_feed_categories' => false,
|
|
'remove_feed_tags' => false,
|
|
'remove_feed_custom_taxonomies' => false,
|
|
'remove_feed_post_types' => false,
|
|
'remove_feed_search' => false,
|
|
'remove_atom_rdf_feeds' => false,
|
|
'remove_shortlinks' => false,
|
|
'remove_rest_api_links' => false,
|
|
'remove_rsd_wlw_links' => false,
|
|
'remove_oembed_links' => false,
|
|
'remove_generator' => false,
|
|
'remove_emoji_scripts' => false,
|
|
'remove_powered_by_header' => false,
|
|
'remove_pingback_header' => false,
|
|
'clean_campaign_tracking_urls' => false,
|
|
'clean_permalinks' => false,
|
|
'clean_permalinks_extra_variables' => '',
|
|
'search_cleanup' => false,
|
|
'search_cleanup_emoji' => false,
|
|
'search_cleanup_patterns' => false,
|
|
'search_character_limit' => 50,
|
|
'deny_search_crawling' => false,
|
|
'deny_wp_json_crawling' => false,
|
|
'deny_adsbot_crawling' => false,
|
|
'redirect_search_pretty_urls' => false,
|
|
'least_readability_ignore_list' => [],
|
|
'least_seo_score_ignore_list' => [],
|
|
'most_linked_ignore_list' => [],
|
|
'least_linked_ignore_list' => [],
|
|
'indexables_page_reading_list' => [ false, false, false, false, false ],
|
|
'indexables_overview_state' => 'dashboard-not-visited',
|
|
'last_known_public_post_types' => [],
|
|
'last_known_public_taxonomies' => [],
|
|
'last_known_no_unindexed' => [],
|
|
'new_post_types' => [],
|
|
'new_taxonomies' => [],
|
|
'show_new_content_type_notification' => false,
|
|
];
|
|
|
|
/**
|
|
* Sub-options which should not be overloaded with multi-site defaults.
|
|
*
|
|
* @var array
|
|
*/
|
|
public $ms_exclude = [
|
|
'ignore_search_engines_discouraged_notice',
|
|
/* Privacy. */
|
|
'baiduverify',
|
|
'googleverify',
|
|
'msverify',
|
|
'yandexverify',
|
|
];
|
|
|
|
/**
|
|
* Possible values for the site_type option.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $site_types = [
|
|
'',
|
|
'blog',
|
|
'shop',
|
|
'news',
|
|
'smallBusiness',
|
|
'corporateOther',
|
|
'personalOther',
|
|
];
|
|
|
|
/**
|
|
* Possible environment types.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $environment_types = [
|
|
'',
|
|
'local',
|
|
'production',
|
|
'staging',
|
|
'development',
|
|
];
|
|
|
|
/**
|
|
* Possible has_multiple_authors options.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $has_multiple_authors_options = [
|
|
'',
|
|
true,
|
|
false,
|
|
];
|
|
|
|
/**
|
|
* Name for an option higher in the hierarchy to override setting access.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $override_option_name = 'wpseo_ms';
|
|
|
|
/**
|
|
* Add the actions and filters for the option.
|
|
*
|
|
* @todo [JRF => testers] Check if the extra actions below would run into problems if an option
|
|
* is updated early on and if so, change the call to schedule these for a later action on add/update
|
|
* instead of running them straight away.
|
|
*/
|
|
protected function __construct() {
|
|
parent::__construct();
|
|
|
|
/**
|
|
* Filter: 'wpseo_enable_tracking' - Enables the data tracking of Yoast SEO Premium.
|
|
*
|
|
* @api string $is_enabled The enabled state. Default is false.
|
|
*/
|
|
$this->defaults['tracking'] = apply_filters( 'wpseo_enable_tracking', false );
|
|
|
|
/* Clear the cache on update/add. */
|
|
add_action( 'add_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] );
|
|
add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] );
|
|
|
|
add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] );
|
|
|
|
/**
|
|
* Filter the `wpseo` option defaults.
|
|
*
|
|
* @param array $defaults Array the defaults for the `wpseo` option attributes.
|
|
*/
|
|
$this->defaults = apply_filters( 'wpseo_option_wpseo_defaults', $this->defaults );
|
|
}
|
|
|
|
/**
|
|
* Get the singleton instance of this class.
|
|
*
|
|
* @return object
|
|
*/
|
|
public static function get_instance() {
|
|
if ( ! ( self::$instance instanceof self ) ) {
|
|
self::$instance = new self();
|
|
}
|
|
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Add filters to make sure that the option is merged with its defaults before being returned.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function add_option_filters() {
|
|
parent::add_option_filters();
|
|
|
|
list( $hookname, $callback, $priority ) = $this->get_verify_features_option_filter_hook();
|
|
|
|
if ( has_filter( $hookname, $callback ) === false ) {
|
|
add_filter( $hookname, $callback, $priority );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove the option filters.
|
|
* Called from the clean_up methods to make sure we retrieve the original old option.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function remove_option_filters() {
|
|
parent::remove_option_filters();
|
|
|
|
list( $hookname, $callback, $priority ) = $this->get_verify_features_option_filter_hook();
|
|
|
|
remove_filter( $hookname, $callback, $priority );
|
|
}
|
|
|
|
/**
|
|
* Add filters to make sure that the option default is returned if the option is not set.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function add_default_filters() {
|
|
parent::add_default_filters();
|
|
|
|
list( $hookname, $callback, $priority ) = $this->get_verify_features_default_option_filter_hook();
|
|
|
|
if ( has_filter( $hookname, $callback ) === false ) {
|
|
add_filter( $hookname, $callback, $priority );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove the default filters.
|
|
* Called from the validate() method to prevent failure to add new options.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function remove_default_filters() {
|
|
parent::remove_default_filters();
|
|
|
|
list( $hookname, $callback, $priority ) = $this->get_verify_features_default_option_filter_hook();
|
|
|
|
remove_filter( $hookname, $callback, $priority );
|
|
}
|
|
|
|
/**
|
|
* Validate the option.
|
|
*
|
|
* @param array $dirty New value for the option.
|
|
* @param array $clean Clean value for the option, normally the defaults.
|
|
* @param array $old Old value of the option.
|
|
*
|
|
* @return array Validated clean value for the option to be saved to the database.
|
|
*/
|
|
protected function validate_option( $dirty, $clean, $old ) {
|
|
|
|
foreach ( $clean as $key => $value ) {
|
|
switch ( $key ) {
|
|
case 'version':
|
|
$clean[ $key ] = WPSEO_VERSION;
|
|
break;
|
|
case 'previous_version':
|
|
case 'semrush_country_code':
|
|
case 'license_server_version':
|
|
case 'home_url':
|
|
case 'zapier_api_key':
|
|
case 'index_now_key':
|
|
case 'wincher_website_id':
|
|
case 'clean_permalinks_extra_variables':
|
|
case 'indexables_overview_state':
|
|
if ( isset( $dirty[ $key ] ) ) {
|
|
$clean[ $key ] = $dirty[ $key ];
|
|
}
|
|
break;
|
|
case 'indexing_reason':
|
|
if ( isset( $dirty[ $key ] ) ) {
|
|
$clean[ $key ] = sanitize_text_field( $dirty[ $key ] );
|
|
}
|
|
break;
|
|
|
|
/* Verification strings. */
|
|
case 'baiduverify':
|
|
case 'googleverify':
|
|
case 'msverify':
|
|
case 'yandexverify':
|
|
$this->validate_verification_string( $key, $dirty, $old, $clean );
|
|
break;
|
|
|
|
/*
|
|
* Boolean dismiss warnings - not fields - may not be in form
|
|
* (and don't need to be either as long as the default is false).
|
|
*/
|
|
case 'ignore_search_engines_discouraged_notice':
|
|
case 'ms_defaults_set':
|
|
if ( isset( $dirty[ $key ] ) ) {
|
|
$clean[ $key ] = WPSEO_Utils::validate_bool( $dirty[ $key ] );
|
|
}
|
|
elseif ( isset( $old[ $key ] ) ) {
|
|
$clean[ $key ] = WPSEO_Utils::validate_bool( $old[ $key ] );
|
|
}
|
|
break;
|
|
|
|
case 'site_type':
|
|
$clean[ $key ] = $old[ $key ];
|
|
if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->site_types, true ) ) {
|
|
$clean[ $key ] = $dirty[ $key ];
|
|
}
|
|
break;
|
|
|
|
case 'environment_type':
|
|
$clean[ $key ] = $old[ $key ];
|
|
if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->environment_types, true ) ) {
|
|
$clean[ $key ] = $dirty[ $key ];
|
|
}
|
|
break;
|
|
|
|
case 'has_multiple_authors':
|
|
$clean[ $key ] = $old[ $key ];
|
|
if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->has_multiple_authors_options, true ) ) {
|
|
$clean[ $key ] = $dirty[ $key ];
|
|
}
|
|
|
|
break;
|
|
|
|
case 'first_activated_on':
|
|
case 'indexing_started':
|
|
case 'activation_redirect_timestamp_free':
|
|
$clean[ $key ] = false;
|
|
if ( isset( $dirty[ $key ] ) ) {
|
|
if ( $dirty[ $key ] === false || WPSEO_Utils::validate_int( $dirty[ $key ] ) ) {
|
|
$clean[ $key ] = $dirty[ $key ];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'tracking':
|
|
$clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : null );
|
|
break;
|
|
|
|
case 'myyoast_oauth':
|
|
case 'semrush_tokens':
|
|
case 'custom_taxonomy_slugs':
|
|
case 'zapier_subscription':
|
|
case 'wincher_tokens':
|
|
case 'workouts_data':
|
|
case 'configuration_finished_steps':
|
|
case 'least_readability_ignore_list':
|
|
case 'least_seo_score_ignore_list':
|
|
case 'most_linked_ignore_list':
|
|
case 'least_linked_ignore_list':
|
|
case 'indexables_page_reading_list':
|
|
case 'last_known_public_post_types':
|
|
case 'last_known_public_taxonomies':
|
|
case 'new_post_types':
|
|
case 'new_taxonomies':
|
|
$clean[ $key ] = $old[ $key ];
|
|
|
|
if ( isset( $dirty[ $key ] ) ) {
|
|
$items = $dirty[ $key ];
|
|
if ( ! is_array( $items ) ) {
|
|
$items = json_decode( $dirty[ $key ], true );
|
|
}
|
|
|
|
if ( is_array( $items ) ) {
|
|
$clean[ $key ] = $dirty[ $key ];
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case 'permalink_structure':
|
|
case 'category_base_url':
|
|
case 'tag_base_url':
|
|
if ( isset( $dirty[ $key ] ) ) {
|
|
$clean[ $key ] = sanitize_option( $key, $dirty[ $key ] );
|
|
}
|
|
break;
|
|
|
|
case 'search_character_limit':
|
|
if ( isset( $dirty[ $key ] ) ) {
|
|
$clean[ $key ] = (int) $dirty[ $key ];
|
|
}
|
|
break;
|
|
|
|
case 'import_cursors':
|
|
case 'importing_completed':
|
|
if ( isset( $dirty[ $key ] ) && is_array( $dirty[ $key ] ) ) {
|
|
$clean[ $key ] = $dirty[ $key ];
|
|
}
|
|
break;
|
|
|
|
case 'wordproof_integration_active':
|
|
$clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false );
|
|
// If the setting has changed, record it.
|
|
if ( $old[ $key ] !== $clean[ $key ] ) {
|
|
$clean['wordproof_integration_changed'] = true;
|
|
}
|
|
break;
|
|
case 'last_known_no_unindexed':
|
|
$clean[ $key ] = $old[ $key ];
|
|
|
|
if ( isset( $dirty[ $key ] ) ) {
|
|
$items = $dirty[ $key ];
|
|
|
|
if ( is_array( $items ) ) {
|
|
foreach ( $items as $item_key => $item ) {
|
|
if ( ! \is_string( $item_key ) || ! \is_numeric( $item ) ) {
|
|
unset( $items[ $item_key ] );
|
|
}
|
|
}
|
|
$clean[ $key ] = $items;
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
/*
|
|
* Boolean (checkbox) fields.
|
|
*
|
|
* Covers:
|
|
* 'disableadvanced_meta'
|
|
* 'enable_headless_rest_endpoints'
|
|
* 'yoast_tracking'
|
|
* 'dynamic_permalinks'
|
|
* 'indexing_first_time'
|
|
* 'first_time_install'
|
|
* 'remove_feed_global'
|
|
* 'remove_feed_global_comments'
|
|
* 'remove_feed_post_comments'
|
|
* 'remove_feed_authors'
|
|
* 'remove_feed_categories'
|
|
* 'remove_feed_tags'
|
|
* 'remove_feed_custom_taxonomies'
|
|
* 'remove_feed_post_types'
|
|
* 'remove_feed_search'
|
|
* 'remove_atom_rdf_feeds'
|
|
* 'remove_shortlinks'
|
|
* 'remove_rest_api_links'
|
|
* 'remove_rsd_wlw_links'
|
|
* 'remove_oembed_links'
|
|
* 'remove_generator'
|
|
* 'remove_emoji_scripts'
|
|
* 'remove_powered_by_header'
|
|
* 'remove_pingback_header'
|
|
* 'clean_campaign_tracking_urls'
|
|
* 'clean_permalinks'
|
|
* 'clean_permalinks_extra_variables'
|
|
* 'search_cleanup'
|
|
* 'search_cleanup_emoji'
|
|
* 'search_cleanup_patterns'
|
|
* 'deny_wp_json_crawling'
|
|
* 'deny_adsbot_crawling'
|
|
* 'redirect_search_pretty_urls'
|
|
* 'should_redirect_after_install_free'
|
|
* 'show_new_content_type_notification'
|
|
* and most of the feature variables.
|
|
*/
|
|
default:
|
|
$clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false );
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $clean;
|
|
}
|
|
|
|
/**
|
|
* Verifies that the feature variables are turned off if the network is configured so.
|
|
*
|
|
* @param mixed $options Value of the option to be returned. Typically an array.
|
|
*
|
|
* @return mixed Filtered $options value.
|
|
*/
|
|
public function verify_features_against_network( $options = [] ) {
|
|
if ( ! is_array( $options ) || empty( $options ) ) {
|
|
return $options;
|
|
}
|
|
|
|
// For the feature variables, set their values to off in case they are disabled.
|
|
$feature_vars = [
|
|
'disableadvanced_meta' => false,
|
|
'ryte_indexability' => false,
|
|
'content_analysis_active' => false,
|
|
'keyword_analysis_active' => false,
|
|
'inclusive_language_analysis_active' => false,
|
|
'enable_admin_bar_menu' => false,
|
|
'enable_cornerstone_content' => false,
|
|
'enable_xml_sitemap' => false,
|
|
'enable_text_link_counter' => false,
|
|
'enable_metabox_insights' => false,
|
|
'enable_link_suggestions' => false,
|
|
'enable_headless_rest_endpoints' => false,
|
|
'tracking' => false,
|
|
'enable_enhanced_slack_sharing' => false,
|
|
'semrush_integration_active' => false,
|
|
'zapier_integration_active' => false,
|
|
'wincher_integration_active' => false,
|
|
'remove_feed_global' => false,
|
|
'remove_feed_global_comments' => false,
|
|
'remove_feed_post_comments' => false,
|
|
'enable_index_now' => false,
|
|
'enable_ai_generator' => false,
|
|
'remove_feed_authors' => false,
|
|
'remove_feed_categories' => false,
|
|
'remove_feed_tags' => false,
|
|
'remove_feed_custom_taxonomies' => false,
|
|
'remove_feed_post_types' => false,
|
|
'remove_feed_search' => false,
|
|
'remove_atom_rdf_feeds' => false,
|
|
'remove_shortlinks' => false,
|
|
'remove_rest_api_links' => false,
|
|
'remove_rsd_wlw_links' => false,
|
|
'remove_oembed_links' => false,
|
|
'remove_generator' => false,
|
|
'remove_emoji_scripts' => false,
|
|
'remove_powered_by_header' => false,
|
|
'remove_pingback_header' => false,
|
|
'clean_campaign_tracking_urls' => false,
|
|
'clean_permalinks' => false,
|
|
'search_cleanup' => false,
|
|
'search_cleanup_emoji' => false,
|
|
'search_cleanup_patterns' => false,
|
|
'redirect_search_pretty_urls' => false,
|
|
'algolia_integration_active' => false,
|
|
];
|
|
|
|
// We can reuse this logic from the base class with the above defaults to parse with the correct feature values.
|
|
$options = $this->prevent_disabled_options_update( $options, $feature_vars );
|
|
|
|
return $options;
|
|
}
|
|
|
|
/**
|
|
* Gets the filter hook name and callback for adjusting the retrieved option value
|
|
* against the network-allowed features.
|
|
*
|
|
* @return array Array where the first item is the hook name, the second is the hook callback,
|
|
* and the third is the hook priority.
|
|
*/
|
|
protected function get_verify_features_option_filter_hook() {
|
|
return [
|
|
"option_{$this->option_name}",
|
|
[ $this, 'verify_features_against_network' ],
|
|
11,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Gets the filter hook name and callback for adjusting the default option value against the network-allowed features.
|
|
*
|
|
* @return array Array where the first item is the hook name, the second is the hook callback,
|
|
* and the third is the hook priority.
|
|
*/
|
|
protected function get_verify_features_default_option_filter_hook() {
|
|
return [
|
|
"default_option_{$this->option_name}",
|
|
[ $this, 'verify_features_against_network' ],
|
|
11,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Clean a given option value.
|
|
*
|
|
* @param array $option_value Old (not merged with defaults or filtered) option value to
|
|
* clean according to the rules for this option.
|
|
* @param string|null $current_version Optional. Version from which to upgrade, if not set,
|
|
* version specific upgrades will be disregarded.
|
|
* @param array|null $all_old_option_values Optional. Only used when importing old options to have
|
|
* access to the real old values, in contrast to the saved ones.
|
|
*
|
|
* @return array Cleaned option.
|
|
*/
|
|
protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) {
|
|
// Deal with value change from text string to boolean.
|
|
$value_change = [
|
|
'ignore_search_engines_discouraged_notice',
|
|
];
|
|
|
|
$target_values = [
|
|
'ignore',
|
|
'done',
|
|
];
|
|
|
|
foreach ( $value_change as $key ) {
|
|
if ( isset( $option_value[ $key ] )
|
|
&& in_array( $option_value[ $key ], $target_values, true )
|
|
) {
|
|
$option_value[ $key ] = true;
|
|
}
|
|
}
|
|
|
|
return $option_value;
|
|
}
|
|
}
|