'user_id'
);
/**
* Filters whether an empty comment should be allowed.
*
* @since 5.1.0
*
* @param bool $allow_empty_comment Whether to allow empty comments. Default false.
* @param array $commentdata Array of comment data to be sent to wp_insert_comment().
*/
$allow_empty_comment = apply_filters( 'allow_empty_comment', false, $commentdata );
if ( '' === $comment_content && ! $allow_empty_comment ) {
return new WP_Error( 'require_valid_comment', __( 'Error: Please type your comment text.' ), 200 );
}
$check_max_lengths = wp_check_comment_data_max_lengths( $commentdata );
if ( is_wp_error( $check_max_lengths ) ) {
return $check_max_lengths;
}
$comment_id = wp_new_comment( wp_slash( $commentdata ), true );
if ( is_wp_error( $comment_id ) ) {
return $comment_id;
}
if ( ! $comment_id ) {
return new WP_Error( 'comment_save_error', __( 'Error: The comment could not be saved. Please try again later.' ), 500 );
}
return get_comment( $comment_id );
}
/**
* Registers the personal data exporter for comments.
*
* @since 4.9.6
*
* @param array[] $exporters An array of personal data exporters.
* @return array[] An array of personal data exporters.
*/
function wp_register_comment_personal_data_exporter( $exporters ) {
$exporters['wordpress-comments'] = array(
'exporter_friendly_name' => __( 'WordPress Comments' ),
'callback' => 'wp_comments_personal_data_exporter',
);
return $exporters;
}
/**
* Finds and exports personal data associated with an email address from the comments table.
*
* @since 4.9.6
*
* @param string $email_address The comment author email address.
* @param int $page Comment page number.
* @return array {
* An array of personal data.
*
* @type array[] $data An array of personal data arrays.
* @type bool $done Whether the exporter is finished.
* }
*/
function wp_comments_personal_data_exporter( $email_address, $page = 1 ) {
// Limit us to 500 comments at a time to avoid timing out.
$number = 500;
$page = (int) $page;
$data_to_export = array();
$comments = get_comments(
array(
'author_email' => $email_address,
'number' => $number,
'paged' => $page,
'orderby' => 'comment_ID',
'order' => 'ASC',
'update_comment_meta_cache' => false,
)
);
$comment_prop_to_export = array(
'comment_author' => __( 'Comment Author' ),
'comment_author_email' => __( 'Comment Author Email' ),
'comment_author_url' => __( 'Comment Author URL' ),
'comment_author_IP' => __( 'Comment Author IP' ),
'comment_agent' => __( 'Comment Author User Agent' ),
'comment_date' => __( 'Comment Date' ),
'comment_content' => __( 'Comment Content' ),
'comment_link' => __( 'Comment URL' ),
);
foreach ( (array) $comments as $comment ) {
$comment_data_to_export = array();
foreach ( $comment_prop_to_export as $key => $name ) {
$value = '';
switch ( $key ) {
case 'comment_author':
case 'comment_author_email':
case 'comment_author_url':
case 'comment_author_IP':
case 'comment_agent':
case 'comment_date':
$value = $comment->{$key};
break;
case 'comment_content':
$value = get_comment_text( $comment->comment_ID );
break;
case 'comment_link':
$value = get_comment_link( $comment->comment_ID );
$value = sprintf(
'%s',
esc_url( $value ),
esc_html( $value )
);
break;
}
if ( ! empty( $value ) ) {
$comment_data_to_export[] = array(
'name' => $name,
'value' => $value,
);
}
}
$data_to_export[] = array(
'group_id' => 'comments',
'group_label' => __( 'Comments' ),
'group_description' => __( 'User’s comment data.' ),
'item_id' => "comment-{$comment->comment_ID}",
'data' => $comment_data_to_export,
);
}
$done = count( $comments ) < $number;
return array(
'data' => $data_to_export,
'done' => $done,
);
}
/**
* Registers the personal data eraser for comments.
*
* @since 4.9.6
*
* @param array $erasers An array of personal data erasers.
* @return array An array of personal data erasers.
*/
function wp_register_comment_personal_data_eraser( $erasers ) {
$erasers['wordpress-comments'] = array(
'eraser_friendly_name' => __( 'WordPress Comments' ),
'callback' => 'wp_comments_personal_data_eraser',
);
return $erasers;
}
/**
* Erases personal data associated with an email address from the comments table.
*
* @since 4.9.6
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $email_address The comment author email address.
* @param int $page Comment page number.
* @return array {
* Data removal results.
*
* @type bool $items_removed Whether items were actually removed.
* @type bool $items_retained Whether items were retained.
* @type string[] $messages An array of messages to add to the personal data export file.
* @type bool $done Whether the eraser is finished.
* }
*/
function wp_comments_personal_data_eraser( $email_address, $page = 1 ) {
global $wpdb;
if ( empty( $email_address ) ) {
return array(
'items_removed' => false,
'items_retained' => false,
'messages' => array(),
'done' => true,
);
}
// Limit us to 500 comments at a time to avoid timing out.
$number = 500;
$page = (int) $page;
$items_removed = false;
$items_retained = false;
$comments = get_comments(
array(
'author_email' => $email_address,
'number' => $number,
'paged' => $page,
'orderby' => 'comment_ID',
'order' => 'ASC',
'include_unapproved' => true,
)
);
/* translators: Name of a comment's author after being anonymized. */
$anon_author = __( 'Anonymous' );
$messages = array();
foreach ( (array) $comments as $comment ) {
$anonymized_comment = array();
$anonymized_comment['comment_agent'] = '';
$anonymized_comment['comment_author'] = $anon_author;
$anonymized_comment['comment_author_email'] = '';
$anonymized_comment['comment_author_IP'] = wp_privacy_anonymize_data( 'ip', $comment->comment_author_IP );
$anonymized_comment['comment_author_url'] = '';
$anonymized_comment['user_id'] = 0;
$comment_id = (int) $comment->comment_ID;
/**
* Filters whether to anonymize the comment.
*
* @since 4.9.6
*
* @param bool|string $anon_message Whether to apply the comment anonymization (bool) or a custom
* message (string). Default true.
* @param WP_Comment $comment WP_Comment object.
* @param array $anonymized_comment Anonymized comment data.
*/
$anon_message = apply_filters( 'wp_anonymize_comment', true, $comment, $anonymized_comment );
if ( true !== $anon_message ) {
if ( $anon_message && is_string( $anon_message ) ) {
$messages[] = esc_html( $anon_message );
} else {
/* translators: %d: Comment ID. */
$messages[] = sprintf( __( 'Comment %d contains personal data but could not be anonymized.' ), $comment_id );
}
$items_retained = true;
continue;
}
$args = array(
'comment_ID' => $comment_id,
);
$updated = $wpdb->update( $wpdb->comments, $anonymized_comment, $args );
if ( $updated ) {
$items_removed = true;
clean_comment_cache( $comment_id );
} else {
$items_retained = true;
}
}
$done = count( $comments ) < $number;
return array(
'items_removed' => $items_removed,
'items_retained' => $items_retained,
'messages' => $messages,
'done' => $done,
);
}
/**
* Sets the last changed time for the 'comment' cache group.
*
* @since 5.0.0
*/
function wp_cache_set_comments_last_changed() {
wp_cache_set_last_changed( 'comment' );
}
/**
* Updates the comment type for a batch of comments.
*
* @since 5.5.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*/
function _wp_batch_update_comment_type() {
global $wpdb;
$lock_name = 'update_comment_type.lock';
// Try to lock.
$lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_name, time() ) );
if ( ! $lock_result ) {
$lock_result = get_option( $lock_name );
// Bail if we were unable to create a lock, or if the existing lock is still valid.
if ( ! $lock_result || ( $lock_result > ( time() - HOUR_IN_SECONDS ) ) ) {
wp_schedule_single_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'wp_update_comment_type_batch' );
return;
}
}
// Update the lock, as by this point we've definitely got a lock, just need to fire the actions.
update_option( $lock_name, time() );
// Check if there's still an empty comment type.
$empty_comment_type = $wpdb->get_var(
"SELECT comment_ID FROM $wpdb->comments
WHERE comment_type = ''
LIMIT 1"
);
// No empty comment type, we're done here.
if ( ! $empty_comment_type ) {
update_option( 'finished_updating_comment_type', true );
delete_option( $lock_name );
return;
}
// Empty comment type found? We'll need to run this script again.
wp_schedule_single_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'wp_update_comment_type_batch' );
/**
* Filters the comment batch size for updating the comment type.
*
* @since 5.5.0
*
* @param int $comment_batch_size The comment batch size. Default 100.
*/
$comment_batch_size = (int) apply_filters( 'wp_update_comment_type_batch_size', 100 );
// Get the IDs of the comments to update.
$comment_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT comment_ID
FROM {$wpdb->comments}
WHERE comment_type = ''
ORDER BY comment_ID DESC
LIMIT %d",
$comment_batch_size
)
);
if ( $comment_ids ) {
$comment_id_list = implode( ',', $comment_ids );
// Update the `comment_type` field value to be `comment` for the next batch of comments.
$wpdb->query(
"UPDATE {$wpdb->comments}
SET comment_type = 'comment'
WHERE comment_type = ''
AND comment_ID IN ({$comment_id_list})" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
// Make sure to clean the comment cache.
clean_comment_cache( $comment_ids );
}
delete_option( $lock_name );
}
/**
* In order to avoid the _wp_batch_update_comment_type() job being accidentally removed,
* check that it's still scheduled while we haven't finished updating comment types.
*
* @ignore
* @since 5.5.0
*/
function _wp_check_for_scheduled_update_comment_type() {
if ( ! get_option( 'finished_updating_comment_type' ) && ! wp_next_scheduled( 'wp_update_comment_type_batch' ) ) {
wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'wp_update_comment_type_batch' );
}
}
/**
* Register initial note status meta.
*
* @since 6.9.0
*/
function wp_create_initial_comment_meta() {
register_meta(
'comment',
'_wp_note_status',
array(
'type' => 'string',
'description' => __( 'Note resolution status' ),
'single' => true,
'show_in_rest' => array(
'schema' => array(
'type' => 'string',
'enum' => array( 'resolved', 'reopen' ),
),
),
'auth_callback' => function ( $allowed, $meta_key, $object_id ) {
return current_user_can( 'edit_comment', $object_id );
},
)
);
}
/**
* Strips inline note markers from rendered block output.
*
* Inline notes - notes anchored to a text selection within a block rather than
* the whole block - are anchored in raw block content with
* `...` so the marker survives edits,
* but the public HTML should not expose note metadata. This filter unwraps the
* marker entirely - dropping the `` open tag and its matching closer while
* keeping the marked text - so nothing leaks to the front end. The raw
* `post_content` (and the REST `raw` view, revisions, exports) keeps the marker
* so the editor can re-attach it on reload.
*
* Only note markers are unwrapped: {@see WP_HTML_Tag_Processor::has_class()}
* matches the `wp-note` class by exact token, so a `` a user or plugin
* added (e.g. a `core/text-color` highlight, or an unrelated `wp-note-foo`
* class) is never flagged and survives byte-for-byte with all of its attributes
* intact. A naive regex would be wrong here: a `\bwp-note\b` word boundary also
* matches `wp-note-foo`, which is why the class check goes through the HTML API
* instead.
*
* The HTML API has no public token-removal method yet, so an anonymous
* {@see WP_HTML_Tag_Processor} subclass unwraps each note `` and its
* matching closer directly on the parsed token stream. Walking tokens - rather
* than matching `` with a regex - means a ``-looking sequence inside
* a comment or attribute value can never be mistaken for a real tag, and a
* nesting stack keeps each note opener paired with its own closer so overlapping
* notes and any user highlight `` left intact still resolve correctly.
*
* The low-level {@see WP_HTML_Tag_Processor} is used deliberately, rather than
* the tree-building {@see WP_HTML_Processor}. Note markers live in user-editable
* content, so the markup is not guaranteed to be well formed. On certain
* ill-formed nesting the tree builder aborts, which would leave note markers -
* and their metadata - in the rendered output. Scanning tokens instead removes
* every `wp-note` marker it encounters and degrades gracefully: an unbalanced or
* stray tag is left exactly as it was rather than corrupting surrounding markup.
*
* @since 7.1.0
*
* @param string $block_content Rendered block HTML.
* @return string Block HTML with `wp-note` markers unwrapped.
*/
function wp_strip_inline_note_markers( $block_content ) {
if ( ! str_contains( $block_content, 'wp-note' ) ) {
return $block_content;
}
/*
* Anonymous subclass exposing token removal, which WP_HTML_Tag_Processor
* does not provide publicly yet. Removing the current token via its bookmark
* span unwraps the `` (opener or closer) while keeping the text it
* wraps.
*/
$processor = new class( $block_content ) extends WP_HTML_Tag_Processor {
/**
* Removes the current token, keeping any text it wraps.
*/
public function remove_token(): void {
// Always called after next_tag() returned true, so the bookmark is set.
$this->set_bookmark( 'here' );
$span = $this->bookmarks['here'];
$this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start, $span->length, '' );
}
};
/*
* Walk every ``, tracking note nesting on a stack so each note opener
* pairs with its own closer, and unwrap only the note markers.
*/
$mark_stack = array();
$query = array(
'tag_name' => 'MARK',
'tag_closers' => 'visit',
);
while ( $processor->next_tag( $query ) ) {
if ( $processor->is_tag_closer() ) {
$is_note = array_pop( $mark_stack );
} else {
$is_note = $processor->has_class( 'wp-note' );
$mark_stack[] = $is_note;
}
if ( true === $is_note ) {
$processor->remove_token();
}
}
return $processor->get_updated_html();
}
This should be an ISO 3166-1 alpha-2 country code.
*
* @return array The recommended payment methods list for the payment gateway.
* Empty array if there are none.
*/
public function get_recommended_payment_methods( WC_Payment_Gateway $payment_gateway, string $country_code = '' ): array {
// Bail if the payment gateway does not implement the method.
if ( ! method_exists( $payment_gateway, 'get_recommended_payment_methods' ) ||
! is_callable( array( $payment_gateway, 'get_recommended_payment_methods' ) ) ) {
return array();
}
try {
// Get the "raw" recommended payment methods from the payment gateway.
$recommended_pms = call_user_func( array( $payment_gateway, 'get_recommended_payment_methods' ), $country_code );
if ( ! is_array( $recommended_pms ) ) {
// Bail if the recommended payment methods are not an array.
return array();
}
} catch ( Throwable $e ) {
// Log so we can investigate.
SafeGlobalFunctionProxy::wc_get_logger()->debug(
'Failed to get recommended payment methods: ' . $e->getMessage(),
array(
'gateway' => $payment_gateway->id,
'country' => $country_code,
'source' => 'settings-payments',
'exception' => $e,
)
);
return array();
}
// Validate the received list items.
$recommended_pms = array_filter(
$recommended_pms,
array( $this, 'validate_recommended_payment_method' )
);
// Sort the list.
$recommended_pms = $this->sort_recommended_payment_methods( $recommended_pms );
// Extract, standardize, and sanitize the details for each recommended payment method.
$standardized_pms = array();
foreach ( $recommended_pms as $index => $recommended_pm ) {
// Use the index as the order since we sorted (and normalized) the list earlier.
$standardized_pms[] = $this->standardize_recommended_payment_method( $recommended_pm, $index );
}
return $standardized_pms;
}
/**
* Validate a recommended payment method entry.
*
* @param mixed $recommended_pm The recommended payment method entry to validate.
*
* @return bool True if the recommended payment method entry is valid, false otherwise.
*/
protected function validate_recommended_payment_method( $recommended_pm ): bool {
// We require at least `id` and `title`.
return is_array( $recommended_pm ) &&
! empty( $recommended_pm['id'] ) &&
! empty( $recommended_pm['title'] );
}
/**
* Sort the recommended payment methods.
*
* @param array $recommended_pms The recommended payment methods list to sort.
*
* @return array The sorted recommended payment methods list.
* List keys are not preserved.
*/
protected function sort_recommended_payment_methods( array $recommended_pms ): array {
// Sort the recommended payment methods by order/priority, if available.
usort(
$recommended_pms,
function ( $a, $b ) {
// `order` takes precedence over `priority`.
// Entries that don't have the order/priority are placed at the end.
return array( ( $a['order'] ?? PHP_INT_MAX ), ( $a['priority'] ?? PHP_INT_MAX ) ) <=> array( ( $b['order'] ?? PHP_INT_MAX ), ( $b['priority'] ?? PHP_INT_MAX ) );
}
);
return array_values( $recommended_pms );
}
/**
* Standardize a recommended payment method entry.
*
* @param array $recommended_pm The recommended payment method entry to standardize.
* @param int $order Optional. The order of the recommended payment method.
* Defaults to 0 if not provided.
*
* @return array The standardized recommended payment method entry.
*/
protected function standardize_recommended_payment_method( array $recommended_pm, int $order = 0 ): array {
$standard_details = array(
'id' => sanitize_key( $recommended_pm['id'] ),
'_order' => $order,
// Default to enabled if not explicit.
'enabled' => wc_string_to_bool( $recommended_pm['enabled'] ?? true ),
// Default to not required if not explicit.
'required' => wc_string_to_bool( $recommended_pm['required'] ?? false ),
'title' => sanitize_text_field( $recommended_pm['title'] ),
'description' => '',
'icon' => '',
'category' => self::PAYMENT_METHOD_CATEGORY_PRIMARY, // Default to primary.
'notice' => array(
'badge' => '',
'message' => '',
'link_text' => '',
'link_url' => '',
),
);
// If the payment method has a description, sanitize it before use.
if ( ! empty( $recommended_pm['description'] ) ) {
$standard_details['description'] = (string) $recommended_pm['description'];
// Make sure that if we have HTML tags, we only allow stylistic tags and anchors.
if ( preg_match( '/<[^>]+>/', $standard_details['description'] ) ) {
// Only allow stylistic tags with a few modifications.
$allowed_tags = wp_kses_allowed_html( 'data' );
$allowed_tags = array_merge(
$allowed_tags,
array(
'a' => array(
'href' => true,
'target' => true,
),
)
);
$standard_details['description'] = wp_kses( $standard_details['description'], $allowed_tags );
}
}
// If the payment method has an icon, try to use it.
if ( ! empty( $recommended_pm['icon'] ) && wc_is_valid_url( $recommended_pm['icon'] ) ) {
$standard_details['icon'] = sanitize_url( $recommended_pm['icon'] );
}
// If the payment method has a category, use it if it's one of the known categories.
if ( ! empty( $recommended_pm['category'] ) &&
in_array( $recommended_pm['category'], array( self::PAYMENT_METHOD_CATEGORY_PRIMARY, self::PAYMENT_METHOD_CATEGORY_SECONDARY ), true ) ) {
$standard_details['category'] = $recommended_pm['category'];
}
// If the payment method has a notice, sanitize and use its fields.
if ( ! empty( $recommended_pm['notice'] ) && is_array( $recommended_pm['notice'] ) ) {
$notice = $recommended_pm['notice'];
if ( ! empty( $notice['badge'] ) ) {
$standard_details['notice']['badge'] = sanitize_text_field( $notice['badge'] );
}
if ( ! empty( $notice['message'] ) ) {
$standard_details['notice']['message'] = sanitize_text_field( $notice['message'] );
}
if ( ! empty( $notice['link_text'] ) ) {
$standard_details['notice']['link_text'] = sanitize_text_field( $notice['link_text'] );
}
if ( ! empty( $notice['link_url'] ) && is_string( $notice['link_url'] ) && wc_is_valid_url( $notice['link_url'] ) ) {
$standard_details['notice']['link_url'] = sanitize_url( $notice['link_url'] );
}
}
return $standard_details;
}
/**
* Get the filename of the payment gateway class.
*
* @param WC_Payment_Gateway $payment_gateway The payment gateway object.
*
* @return string|null The filename of the payment gateway class or null if it cannot be determined.
*/
private function get_class_filename( WC_Payment_Gateway $payment_gateway ): ?string {
// If the payment gateway object has a `class_filename` property, use it.
// It is only used in development environments (including when running tests).
if ( isset( $payment_gateway->class_filename ) && in_array( wp_get_environment_type(), array( 'local', 'development' ), true ) ) {
$class_filename = $payment_gateway->class_filename;
} else {
try {
$reflector = new \ReflectionClass( get_class( $payment_gateway ) );
$class_filename = $reflector->getFileName();
} catch ( Throwable $e ) {
// Bail but log so we can investigate.
SafeGlobalFunctionProxy::wc_get_logger()->debug(
'Failed to get gateway class filename: ' . $e->getMessage(),
array(
'gateway' => $payment_gateway->id,
'source' => 'settings-payments',
'exception' => $e,
)
);
return null;
}
}
// Bail if we couldn't get the gateway class filename.
if ( ! is_string( $class_filename ) ) {
return null;
}
return $class_filename;
}
/**
* Get the type of entity the payment gateway class is contained in.
*
* @param WC_Payment_Gateway $payment_gateway The payment gateway object.
*
* @return string The type of extension containing the payment gateway class.
*/
private function get_containing_entity_type( WC_Payment_Gateway $payment_gateway ): string {
global $wp_plugin_paths, $wp_theme_directories;
// If the payment gateway object has a `extension_type` property, use it.
// This is useful for testing.
if ( isset( $payment_gateway->extension_type ) ) {
// Validate the extension type.
if ( ! in_array(
$payment_gateway->extension_type,
array(
PaymentsProviders::EXTENSION_TYPE_WPORG,
PaymentsProviders::EXTENSION_TYPE_MU_PLUGIN,
PaymentsProviders::EXTENSION_TYPE_THEME,
),
true
) ) {
return PaymentsProviders::EXTENSION_TYPE_UNKNOWN;
}
return $payment_gateway->extension_type;
}
$gateway_class_filename = $this->get_class_filename( $payment_gateway );
// Bail if we couldn't get the gateway class filename.
if ( ! is_string( $gateway_class_filename ) ) {
return PaymentsProviders::EXTENSION_TYPE_UNKNOWN;
}
// Plugin paths logic closely matches the one in plugin_basename().
// $wp_plugin_paths contains normalized paths.
$file = wp_normalize_path( $gateway_class_filename );
arsort( $wp_plugin_paths );
// Account for symlinks in the plugin paths.
foreach ( $wp_plugin_paths as $dir => $realdir ) {
if ( str_starts_with( $file, $realdir ) ) {
$gateway_class_filename = $dir . substr( $gateway_class_filename, strlen( $realdir ) );
}
}
// Test for regular plugins.
if ( str_starts_with( $gateway_class_filename, wp_normalize_path( WP_PLUGIN_DIR ) ) ) {
// For now, all plugins are considered WordPress.org plugins.
return PaymentsProviders::EXTENSION_TYPE_WPORG;
}
// Test for must-use plugins.
if ( str_starts_with( $gateway_class_filename, wp_normalize_path( WPMU_PLUGIN_DIR ) ) ) {
return PaymentsProviders::EXTENSION_TYPE_MU_PLUGIN;
}
// Check if it is part of a theme.
if ( is_array( $wp_theme_directories ) ) {
foreach ( $wp_theme_directories as $dir ) {
// Check if the class file is in a theme directory.
if ( str_starts_with( $gateway_class_filename, $dir ) ) {
return PaymentsProviders::EXTENSION_TYPE_THEME;
}
}
}
// Default to an unknown type.
return PaymentsProviders::EXTENSION_TYPE_UNKNOWN;
}
/**
* Extract the slug from a given path.
*
* It can be a directory or file path.
* This should be a relative path since the top-level directory or file name will be used as the slug.
*
* @param string $path The path to extract the slug from.
*
* @return string The slug extracted from the path.
*/
private function extract_slug_from_path( string $path ): string {
$path = trim( $path );
$path = trim( $path, DIRECTORY_SEPARATOR );
// If the path is just a file name, use it as the slug.
if ( false === strpos( $path, DIRECTORY_SEPARATOR ) ) {
return Utils::trim_php_file_extension( $path );
}
$parts = explode( DIRECTORY_SEPARATOR, $path );
// Bail if we couldn't get the parts.
if ( ! is_array( $parts ) ) {
return '';
}
return reset( $parts );
}
}