/** * REST API: WP_REST_Comments_Controller class * * @package WordPress * @subpackage REST_API * @since 4.7.0 */ /** * Core controller used to access comments via the REST API. * * @since 4.7.0 * * @see WP_REST_Controller */ class WP_REST_Comments_Controller extends WP_REST_Controller { /** * Instance of a comment meta fields object. * * @since 4.7.0 * @var WP_REST_Comment_Meta_Fields */ protected $meta; /** * Constructor. * * @since 4.7.0 */ public function __construct() { $this->namespace = 'wp/v2'; $this->rest_base = 'comments'; $this->meta = new WP_REST_Comment_Meta_Fields(); } /** * Registers the routes for comments. * * @since 4.7.0 * * @see register_rest_route() */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the comment.' ), 'type' => 'integer', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 'password' => array( 'description' => __( 'The password for the parent post of the comment (if the post is password protected).' ), 'type' => 'string', ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'force' => array( 'type' => 'boolean', 'default' => false, 'description' => __( 'Whether to bypass Trash and force deletion.' ), ), 'password' => array( 'description' => __( 'The password for the parent post of the comment (if the post is password protected).' ), 'type' => 'string', ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Checks if a given request has access to read comments. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has read access, error object otherwise. */ public function get_items_permissions_check( $request ) { if ( ! empty( $request['post'] ) ) { foreach ( (array) $request['post'] as $post_id ) { $post = get_post( $post_id ); if ( ! empty( $post_id ) && $post && ! $this->check_read_post_permission( $post, $request ) ) { return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you are not allowed to read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) ); } elseif ( 0 === $post_id && ! current_user_can( 'moderate_comments' ) ) { return new WP_Error( 'rest_cannot_read', __( 'Sorry, you are not allowed to read comments without a post.' ), array( 'status' => rest_authorization_required_code() ) ); } } } if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) { return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit comments.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( ! current_user_can( 'edit_posts' ) ) { $protected_params = array( 'author', 'author_exclude', 'author_email', 'type', 'status' ); $forbidden_params = array(); foreach ( $protected_params as $param ) { if ( 'status' === $param ) { if ( 'approve' !== $request[ $param ] ) { $forbidden_params[] = $param; } } elseif ( 'type' === $param ) { if ( 'comment' !== $request[ $param ] ) { $forbidden_params[] = $param; } } elseif ( ! empty( $request[ $param ] ) ) { $forbidden_params[] = $param; } } if ( ! empty( $forbidden_params ) ) { return new WP_Error( 'rest_forbidden_param', /* translators: %s: List of forbidden parameters. */ sprintf( __( 'Query parameter not permitted: %s' ), implode( ', ', $forbidden_params ) ), array( 'status' => rest_authorization_required_code() ) ); } } return true; } /** * Retrieves a list of comment items. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or error object on failure. */ public function get_items( $request ) { // Retrieve the list of registered collection query parameters. $registered = $this->get_collection_params(); /* * This array defines mappings between public API query parameters whose * values are accepted as-passed, and their internal WP_Query parameter * name equivalents (some are the same). Only values which are also * present in $registered will be set. */ $parameter_mappings = array( 'author' => 'author__in', 'author_email' => 'author_email', 'author_exclude' => 'author__not_in', 'exclude' => 'comment__not_in', 'include' => 'comment__in', 'offset' => 'offset', 'order' => 'order', 'parent' => 'parent__in', 'parent_exclude' => 'parent__not_in', 'per_page' => 'number', 'post' => 'post__in', 'search' => 'search', 'status' => 'status', 'type' => 'type', ); $prepared_args = array(); /* * For each known parameter which is both registered and present in the request, * set the parameter's value on the query $prepared_args. */ foreach ( $parameter_mappings as $api_param => $wp_param ) { if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { $prepared_args[ $wp_param ] = $request[ $api_param ]; } } // Ensure certain parameter values default to empty strings. foreach ( array( 'author_email', 'search' ) as $param ) { if ( ! isset( $prepared_args[ $param ] ) ) { $prepared_args[ $param ] = ''; } } if ( isset( $registered['orderby'] ) ) { $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] ); } $prepared_args['no_found_rows'] = false; $prepared_args['update_comment_post_cache'] = true; $prepared_args['date_query'] = array(); // Set before into date query. Date query must be specified as an array of an array. if ( isset( $registered['before'], $request['before'] ) ) { $prepared_args['date_query'][0]['before'] = $request['before']; } // Set after into date query. Date query must be specified as an array of an array. if ( isset( $registered['after'], $request['after'] ) ) { $prepared_args['date_query'][0]['after'] = $request['after']; } if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) { $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 ); } /** * Filters WP_Comment_Query arguments when querying comments via the REST API. * * @since 4.7.0 * * @link https://developer.wordpress.org/reference/classes/wp_comment_query/ * * @param array $prepared_args Array of arguments for WP_Comment_Query. * @param WP_REST_Request $request The REST API request. */ $prepared_args = apply_filters( 'rest_comment_query', $prepared_args, $request ); $query = new WP_Comment_Query(); $query_result = $query->query( $prepared_args ); $comments = array(); foreach ( $query_result as $comment ) { if ( ! $this->check_read_permission( $comment, $request ) ) { continue; } $data = $this->prepare_item_for_response( $comment, $request ); $comments[] = $this->prepare_response_for_collection( $data ); } $total_comments = (int) $query->found_comments; $max_pages = (int) $query->max_num_pages; if ( $total_comments < 1 ) { // Out-of-bounds, run the query again without LIMIT for total count. unset( $prepared_args['number'], $prepared_args['offset'] ); $query = new WP_Comment_Query(); $prepared_args['count'] = true; $prepared_args['orderby'] = 'none'; $total_comments = $query->query( $prepared_args ); $max_pages = (int) ceil( $total_comments / $request['per_page'] ); } $response = rest_ensure_response( $comments ); $response->header( 'X-WP-Total', $total_comments ); $response->header( 'X-WP-TotalPages', $max_pages ); $base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); if ( $request['page'] > 1 ) { $prev_page = $request['page'] - 1; if ( $prev_page > $max_pages ) { $prev_page = $max_pages; } $prev_link = add_query_arg( 'page', $prev_page, $base ); $response->link_header( 'prev', $prev_link ); } if ( $max_pages > $request['page'] ) { $next_page = $request['page'] + 1; $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } return $response; } /** * Get the comment, if the ID is valid. * * @since 4.7.2 * * @param int $id Supplied ID. * @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise. */ protected function get_comment( $id ) { $error = new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); if ( (int) $id <= 0 ) { return $error; } $id = (int) $id; $comment = get_comment( $id ); if ( empty( $comment ) ) { return $error; } if ( ! empty( $comment->comment_post_ID ) ) { $post = get_post( (int) $comment->comment_post_ID ); if ( empty( $post ) ) { return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } } return $comment; } /** * Checks if a given request has access to read the comment. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has read access for the item, error object otherwise. */ public function get_item_permissions_check( $request ) { $comment = $this->get_comment( $request['id'] ); if ( is_wp_error( $comment ) ) { return $comment; } if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) { return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit comments.' ), array( 'status' => rest_authorization_required_code() ) ); } $post = get_post( $comment->comment_post_ID ); if ( ! $this->check_read_permission( $comment, $request ) ) { return new WP_Error( 'rest_cannot_read', __( 'Sorry, you are not allowed to read this comment.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( $post && ! $this->check_read_post_permission( $post, $request ) ) { return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you are not allowed to read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Retrieves a comment. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or error object on failure. */ public function get_item( $request ) { $comment = $this->get_comment( $request['id'] ); if ( is_wp_error( $comment ) ) { return $comment; } $data = $this->prepare_item_for_response( $comment, $request ); $response = rest_ensure_response( $data ); return $response; } /** * Checks if a given request has access to create a comment. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to create items, error object otherwise. */ public function create_item_permissions_check( $request ) { if ( ! is_user_logged_in() ) { if ( get_option( 'comment_registration' ) ) { return new WP_Error( 'rest_comment_login_required', __( 'Sorry, you must be logged in to comment.' ), array( 'status' => 401 ) ); } /** * Filters whether comments can be created via the REST API without authentication. * * Enables creating comments for anonymous users. * * @since 4.7.0 * * @param bool $allow_anonymous Whether to allow anonymous comments to * be created. Default `false`. * @param WP_REST_Request $request Request used to generate the * response. */ $allow_anonymous = apply_filters( 'rest_allow_anonymous_comments', false, $request ); if ( ! $allow_anonymous ) { return new WP_Error( 'rest_comment_login_required', __( 'Sorry, you must be logged in to comment.' ), array( 'status' => 401 ) ); } } // Limit who can set comment `author`, `author_ip` or `status` to anything other than the default. if ( isset( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( 'moderate_comments' ) ) { return new WP_Error( 'rest_comment_invalid_author', /* translators: %s: Request parameter. */ sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'author' ), array( 'status' => rest_authorization_required_code() ) ); } if ( isset( $request['author_ip'] ) && ! current_user_can( 'moderate_comments' ) ) { if ( empty( $_SERVER['REMOTE_ADDR'] ) || $request['author_ip'] !== $_SERVER['REMOTE_ADDR'] ) { return new WP_Error( 'rest_comment_invalid_author_ip', /* translators: %s: Request parameter. */ sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'author_ip' ), array( 'status' => rest_authorization_required_code() ) ); } } if ( isset( $request['status'] ) && ! current_user_can( 'moderate_comments' ) ) { return new WP_Error( 'rest_comment_invalid_status', /* translators: %s: Request parameter. */ sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'status' ), array( 'status' => rest_authorization_required_code() ) ); } if ( empty( $request['post'] ) ) { return new WP_Error( 'rest_comment_invalid_post_id', __( 'Sorry, you are not allowed to create this comment without a post.' ), array( 'status' => 403 ) ); } $post = get_post( (int) $request['post'] ); if ( ! $post ) { return new WP_Error( 'rest_comment_invalid_post_id', __( 'Sorry, you are not allowed to create this comment without a post.' ), array( 'status' => 403 ) ); } if ( 'draft' === $post->post_status ) { return new WP_Error( 'rest_comment_draft_post', __( 'Sorry, you are not allowed to create a comment on this post.' ), array( 'status' => 403 ) ); } if ( 'trash' === $post->post_status ) { return new WP_Error( 'rest_comment_trash_post', __( 'Sorry, you are not allowed to create a comment on this post.' ), array( 'status' => 403 ) ); } if ( ! $this->check_read_post_permission( $post, $request ) ) { return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you are not allowed to read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( ! comments_open( $post->ID ) ) { return new WP_Error( 'rest_comment_closed', __( 'Sorry, comments are closed for this item.' ), array( 'status' => 403 ) ); } return true; } /** * Creates a comment. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or error object on failure. */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return new WP_Error( 'rest_comment_exists', __( 'Cannot create existing comment.' ), array( 'status' => 400 ) ); } // Do not allow comments to be created with a non-default type. if ( ! empty( $request['type'] ) && 'comment' !== $request['type'] ) { return new WP_Error( 'rest_invalid_comment_type', __( 'Cannot create a comment with that type.' ), array( 'status' => 400 ) ); } $prepared_comment = $this->prepare_item_for_database( $request ); if ( is_wp_error( $prepared_comment ) ) { return $prepared_comment; } $prepared_comment['comment_type'] = 'comment'; if ( ! isset( $prepared_comment['comment_content'] ) ) { $prepared_comment['comment_content'] = ''; } if ( ! $this->check_is_comment_content_allowed( $prepared_comment ) ) { return new WP_Error( 'rest_comment_content_invalid', __( 'Invalid comment content.' ), array( 'status' => 400 ) ); } // Setting remaining values before wp_insert_comment so we can use wp_allow_comment(). if ( ! isset( $prepared_comment['comment_date_gmt'] ) ) { $prepared_comment['comment_date_gmt'] = current_time( 'mysql', true ); } // Set author data if the user's logged in. $missing_author = empty( $prepared_comment['user_id'] ) && empty( $prepared_comment['comment_author'] ) && empty( $prepared_comment['comment_author_email'] ) && empty( $prepared_comment['comment_author_url'] ); if ( is_user_logged_in() && $missing_author ) { $user = wp_get_current_user(); $prepared_comment['user_id'] = $user->ID; $prepared_comment['comment_author'] = $user->display_name; $prepared_comment['comment_author_email'] = $user->user_email; $prepared_comment['comment_author_url'] = $user->user_url; } // Honor the discussion setting that requires a name and email address of the comment author. if ( get_option( 'require_name_email' ) ) { if ( empty( $prepared_comment['comment_author'] ) || empty( $prepared_comment['comment_author_email'] ) ) { return new WP_Error( 'rest_comment_author_data_required', __( 'Creating a comment requires valid author name and email values.' ), array( 'status' => 400 ) ); } } if ( ! isset( $prepared_comment['comment_author_email'] ) ) { $prepared_comment['comment_author_email'] = ''; } if ( ! isset( $prepared_comment['comment_author_url'] ) ) { $prepared_comment['comment_author_url'] = ''; } if ( ! isset( $prepared_comment['comment_agent'] ) ) { $prepared_comment['comment_agent'] = ''; } $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_comment ); if ( is_wp_error( $check_comment_lengths ) ) { $error_code = $check_comment_lengths->get_error_code(); return new WP_Error( $error_code, __( 'Comment field exceeds maximum length allowed.' ), array( 'status' => 400 ) ); } $prepared_comment['comment_approved'] = wp_allow_comment( $prepared_comment, true ); if ( is_wp_error( $prepared_comment['comment_approved'] ) ) { $error_code = $prepared_comment['comment_approved']->get_error_code(); $error_message = $prepared_comment['comment_approved']->get_error_message(); if ( 'comment_duplicate' === $error_code ) { return new WP_Error( $error_code, $error_message, array( 'status' => 409 ) ); } if ( 'comment_flood' === $error_code ) { return new WP_Error( $error_code, $error_message, array( 'status' => 400 ) ); } return $prepared_comment['comment_approved']; } /** * Filters a comment before it is inserted via the REST API. * * Allows modification of the comment right before it is inserted via wp_insert_comment(). * Returning a WP_Error value from the filter will short-circuit insertion and allow * skipping further processing. * * @since 4.7.0 * @since 4.8.0 `$prepared_comment` can now be a WP_Error to short-circuit insertion. * * @param array|WP_Error $prepared_comment The prepared comment data for wp_insert_comment(). * @param WP_REST_Request $request Request used to insert the comment. */ $prepared_comment = apply_filters( 'rest_pre_insert_comment', $prepared_comment, $request ); if ( is_wp_error( $prepared_comment ) ) { return $prepared_comment; } $comment_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_comment ) ) ); if ( ! $comment_id ) { return new WP_Error( 'rest_comment_failed_create', __( 'Creating comment failed.' ), array( 'status' => 500 ) ); } if ( isset( $request['status'] ) ) { $this->handle_status_param( $request['status'], $comment_id ); } $comment = get_comment( $comment_id ); /** * Fires after a comment is created or updated via the REST API. * * @since 4.7.0 * * @param WP_Comment $comment Inserted or updated comment object. * @param WP_REST_Request $request Request object. * @param bool $creating True when creating a comment, false * when updating. */ do_action( 'rest_insert_comment', $comment, $request, true ); $schema = $this->get_item_schema(); if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { $meta_update = $this->meta->update_value( $request['meta'], $comment_id ); if ( is_wp_error( $meta_update ) ) { return $meta_update; } } $fields_update = $this->update_additional_fields_for_object( $comment, $request ); if ( is_wp_error( $fields_update ) ) { return $fields_update; } $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view'; $request->set_param( 'context', $context ); /** * Fires completely after a comment is created or updated via the REST API. * * @since 5.0.0 * * @param WP_Comment $comment Inserted or updated comment object. * @param WP_REST_Request $request Request object. * @param bool $creating True when creating a comment, false * when updating. */ do_action( 'rest_after_insert_comment', $comment, $request, true ); $response = $this->prepare_item_for_response( $comment, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment_id ) ) ); return $response; } /** * Checks if a given REST request has access to update a comment. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to update the item, error object otherwise. */ public function update_item_permissions_check( $request ) { $comment = $this->get_comment( $request['id'] ); if ( is_wp_error( $comment ) ) { return $comment; } if ( ! $this->check_edit_permission( $comment ) ) { return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this comment.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Updates a comment. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or error object on failure. */ public function update_item( $request ) { $comment = $this->get_comment( $request['id'] ); if ( is_wp_error( $comment ) ) { return $comment; } $id = $comment->comment_ID; if ( isset( $request['type'] ) && get_comment_type( $id ) !== $request['type'] ) { return new WP_Error( 'rest_comment_invalid_type', __( 'Sorry, you are not allowed to change the comment type.' ), array( 'status' => 404 ) ); } $prepared_args = $this->prepare_item_for_database( $request ); if ( is_wp_error( $prepared_args ) ) { return $prepared_args; } if ( ! empty( $prepared_args['comment_post_ID'] ) ) { $post = get_post( $prepared_args['comment_post_ID'] ); if ( empty( $post ) ) { return new WP_Error( 'rest_comment_invalid_post_id', __( 'Invalid post ID.' ), array( 'status' => 403 ) ); } } if ( empty( $prepared_args ) && isset( $request['status'] ) ) { // Only the comment status is being changed. $change = $this->handle_status_param( $request['status'], $id ); if ( ! $change ) { return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment status failed.' ), array( 'status' => 500 ) ); } } elseif ( ! empty( $prepared_args ) ) { if ( is_wp_error( $prepared_args ) ) { return $prepared_args; } if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) { return new WP_Error( 'rest_comment_content_invalid', __( 'Invalid comment content.' ), array( 'status' => 400 ) ); } $prepared_args['comment_ID'] = $id; $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args ); if ( is_wp_error( $check_comment_lengths ) ) { $error_code = $check_comment_lengths->get_error_code(); return new WP_Error( $error_code, __( 'Comment field exceeds maximum length allowed.' ), array( 'status' => 400 ) ); } $updated = wp_update_comment( wp_slash( (array) $prepared_args ), true ); if ( is_wp_error( $updated ) ) { return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment failed.' ), array( 'status' => 500 ) ); } if ( isset( $request['status'] ) ) { $this->handle_status_param( $request['status'], $id ); } } $comment = get_comment( $id ); /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */ do_action( 'rest_insert_comment', $comment, $request, false ); $schema = $this->get_item_schema(); if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { $meta_update = $this->meta->update_value( $request['meta'], $id ); if ( is_wp_error( $meta_update ) ) { return $meta_update; } } $fields_update = $this->update_additional_fields_for_object( $comment, $request ); if ( is_wp_error( $fields_update ) ) { return $fields_update; } $request->set_param( 'context', 'edit' ); /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */ do_action( 'rest_after_insert_comment', $comment, $request, false ); $response = $this->prepare_item_for_response( $comment, $request ); return rest_ensure_response( $response ); } /** * Checks if a given request has access to delete a comment. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to delete the item, error object otherwise. */ public function delete_item_permissions_check( $request ) { $comment = $this->get_comment( $request['id'] ); if ( is_wp_error( $comment ) ) { return $comment; } if ( ! $this->check_edit_permission( $comment ) ) { return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete this comment.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Deletes a comment. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or error object on failure. */ public function delete_item( $request ) { $comment = $this->get_comment( $request['id'] ); if ( is_wp_error( $comment ) ) { return $comment; } $force = isset( $request['force'] ) ? (bool) $request['force'] : false; /** * Filters whether a comment can be trashed via the REST API. * * Return false to disable trash support for the comment. * * @since 4.7.0 * * @param bool $supports_trash Whether the comment supports trashing. * @param WP_Comment $comment The comment object being considered for trashing support. */ $supports_trash = apply_filters( 'rest_comment_trashable', ( EMPTY_TRASH_DAYS > 0 ), $comment ); $request->set_param( 'context', 'edit' ); if ( $force ) { $previous = $this->prepare_item_for_response( $comment, $request ); $result = wp_delete_comment( $comment->comment_ID, true ); $response = new WP_REST_Response(); $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data(), ) ); } else { // If this type doesn't support trashing, error out. if ( ! $supports_trash ) { return new WP_Error( 'rest_trash_not_supported', /* translators: %s: force=true */ sprintf( __( "The comment does not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) ); } if ( 'trash' === $comment->comment_approved ) { return new WP_Error( 'rest_already_trashed', __( 'The comment has already been trashed.' ), array( 'status' => 410 ) ); } $result = wp_trash_comment( $comment->comment_ID ); $comment = get_comment( $comment->comment_ID ); $response = $this->prepare_item_for_response( $comment, $request ); } if ( ! $result ) { return new WP_Error( 'rest_cannot_delete', __( 'The comment cannot be deleted.' ), array( 'status' => 500 ) ); } /** * Fires after a comment is deleted via the REST API. * * @since 4.7.0 * * @param WP_Comment $comment The deleted comment data. * @param WP_REST_Response $response The response returned from the API. * @param WP_REST_Request $request The request sent to the API. */ do_action( 'rest_delete_comment', $comment, $response, $request ); return $response; } /** * Prepares a single comment output for response. * * @since 4.7.0 * @since 5.9.0 Renamed `$comment` to `$item` to match parent class for PHP 8 named parameter support. * * @param WP_Comment $item Comment object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $item, $request ) { // Restores the more descriptive, specific name for use within this method. $comment = $item; $fields = $this->get_fields_for_response( $request ); $data = array(); if ( in_array( 'id', $fields, true ) ) { $data['id'] = (int) $comment->comment_ID; } if ( in_array( 'post', $fields, true ) ) { $data['post'] = (int) $comment->comment_post_ID; } if ( in_array( 'parent', $fields, true ) ) { $data['parent'] = (int) $comment->comment_parent; } if ( in_array( 'author', $fields, true ) ) { $data['author'] = (int) $comment->user_id; } if ( in_array( 'author_name', $fields, true ) ) { $data['author_name'] = $comment->comment_author; } if ( in_array( 'author_email', $fields, true ) ) { $data['author_email'] = $comment->comment_author_email; } if ( in_array( 'author_url', $fields, true ) ) { $data['author_url'] = $comment->comment_author_url; } if ( in_array( 'author_ip', $fields, true ) ) { $data['author_ip'] = $comment->comment_author_IP; } if ( in_array( 'author_user_agent', $fields, true ) ) { $data['author_user_agent'] = $comment->comment_agent; } if ( in_array( 'date', $fields, true ) ) { $data['date'] = mysql_to_rfc3339( $comment->comment_date ); } if ( in_array( 'date_gmt', $fields, true ) ) { $data['date_gmt'] = mysql_to_rfc3339( $comment->comment_date_gmt ); } if ( in_array( 'content', $fields, true ) ) { $data['content'] = array( /** This filter is documented in wp-includes/comment-template.php */ 'rendered' => apply_filters( 'comment_text', $comment->comment_content, $comment, array() ), 'raw' => $comment->comment_content, ); } if ( in_array( 'link', $fields, true ) ) { $data['link'] = get_comment_link( $comment ); } if ( in_array( 'status', $fields, true ) ) { $data['status'] = $this->prepare_status_response( $comment->comment_approved ); } if ( in_array( 'type', $fields, true ) ) { $data['type'] = get_comment_type( $comment->comment_ID ); } if ( in_array( 'author_avatar_urls', $fields, true ) ) { $data['author_avatar_urls'] = rest_get_avatar_urls( $comment ); } if ( in_array( 'meta', $fields, true ) ) { $data['meta'] = $this->meta->get_value( $comment->comment_ID, $request ); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { $response->add_links( $this->prepare_links( $comment ) ); } /** * Filters a comment returned from the REST API. * * Allows modification of the comment right before it is returned. * * @since 4.7.0 * * @param WP_REST_Response $response The response object. * @param WP_Comment $comment The original comment object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'rest_prepare_comment', $response, $comment, $request ); } /** * Prepares links for the request. * * @since 4.7.0 * * @param WP_Comment $comment Comment object. * @return array Links for the given comment. */ protected function prepare_links( $comment ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_ID ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), ), ); if ( 0 !== (int) $comment->user_id ) { $links['author'] = array( 'href' => rest_url( 'wp/v2/users/' . $comment->user_id ), 'embeddable' => true, ); } if ( 0 !== (int) $comment->comment_post_ID ) { $post = get_post( $comment->comment_post_ID ); $post_route = rest_get_route_for_post( $post ); if ( ! empty( $post->ID ) && $post_route ) { $links['up'] = array( 'href' => rest_url( $post_route ), 'embeddable' => true, 'post_type' => $post->post_type, ); } } if ( 0 !== (int) $comment->comment_parent ) { $links['in-reply-to'] = array( 'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_parent ) ), 'embeddable' => true, ); } // Only grab one comment to verify the comment has children. $comment_children = $comment->get_children( array( 'count' => true, 'orderby' => 'none', ) ); if ( ! empty( $comment_children ) ) { $args = array( 'parent' => $comment->comment_ID, ); $rest_url = add_query_arg( $args, rest_url( $this->namespace . '/' . $this->rest_base ) ); $links['children'] = array( 'href' => $rest_url, 'embeddable' => true, ); } return $links; } /** * Prepends internal property prefix to query parameters to match our response fields. * * @since 4.7.0 * * @param string $query_param Query parameter. * @return string The normalized query parameter. */ protected function normalize_query_param( $query_param ) { $prefix = 'comment_'; switch ( $query_param ) { case 'id': $normalized = $prefix . 'ID'; break; case 'post': $normalized = $prefix . 'post_ID'; break; case 'parent': $normalized = $prefix . 'parent'; break; case 'include': $normalized = 'comment__in'; break; default: $normalized = $prefix . $query_param; break; } return $normalized; } /** * Checks comment_approved to set comment status for single comment output. * * @since 4.7.0 * * @param string|int $comment_approved comment status. * @return string Comment status. */ protected function prepare_status_response( $comment_approved ) { switch ( $comment_approved ) { case 'hold': case '0': $status = 'hold'; break; case 'approve': case '1': $status = 'approved'; break; case 'spam': case 'trash': default: $status = $comment_approved; break; } return $status; } /** * Prepares a single comment to be inserted into the database. * * @since 4.7.0 * * @param WP_REST_Request $request Request object. * @return array|WP_Error Prepared comment, otherwise WP_Error object. */ protected function prepare_item_for_database( $request ) { $prepared_comment = array(); /* * Allow the comment_content to be set via the 'content' or * the 'content.raw' properties of the Request object. */ if ( isset( $request['content'] ) && is_string( $request['content'] ) ) { $prepared_comment['comment_content'] = trim( $request['content'] ); } elseif ( isset( $request['content']['raw'] ) && is_string( $request['content']['raw'] ) ) { $prepared_comment['comment_content'] = trim( $request['content']['raw'] ); } if ( isset( $request['post'] ) ) { $prepared_comment['comment_post_ID'] = (int) $request['post']; } if ( isset( $request['parent'] ) ) { $prepared_comment['comment_parent'] = $request['parent']; } if ( isset( $request['author'] ) ) { $user = new WP_User( $request['author'] ); if ( $user->exists() ) { $prepared_comment['user_id'] = $user->ID; $prepared_comment['comment_author'] = $user->display_name; $prepared_comment['comment_author_email'] = $user->user_email; $prepared_comment['comment_author_url'] = $user->user_url; } else { return new WP_Error( 'rest_comment_author_invalid', __( 'Invalid comment author ID.' ), array( 'status' => 400 ) ); } } if ( isset( $request['author_name'] ) ) { $prepared_comment['comment_author'] = $request['author_name']; } if ( isset( $request['author_email'] ) ) { $prepared_comment['comment_author_email'] = $request['author_email']; } if ( isset( $request['author_url'] ) ) { $prepared_comment['comment_author_url'] = $request['author_url']; } if ( isset( $request['author_ip'] ) && current_user_can( 'moderate_comments' ) ) { $prepared_comment['comment_author_IP'] = $request['author_ip']; } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( $_SERVER['REMOTE_ADDR'] ) ) { $prepared_comment['comment_author_IP'] = $_SERVER['REMOTE_ADDR']; } else { $prepared_comment['comment_author_IP'] = '127.0.0.1'; } if ( ! empty( $request['author_user_agent'] ) ) { $prepared_comment['comment_agent'] = $request['author_user_agent']; } elseif ( $request->get_header( 'user_agent' ) ) { $prepared_comment['comment_agent'] = $request->get_header( 'user_agent' ); } if ( ! empty( $request['date'] ) ) { $date_data = rest_get_date_with_gmt( $request['date'] ); if ( ! empty( $date_data ) ) { list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data; } } elseif ( ! empty( $request['date_gmt'] ) ) { $date_data = rest_get_date_with_gmt( $request['date_gmt'], true ); if ( ! empty( $date_data ) ) { list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data; } } /** * Filters a comment added via the REST API after it is prepared for insertion into the database. * * Allows modification of the comment right after it is prepared for the database. * * @since 4.7.0 * * @param array $prepared_comment The prepared comment data for `wp_insert_comment`. * @param WP_REST_Request $request The current request. */ return apply_filters( 'rest_preprocess_comment', $prepared_comment, $request ); } /** * Retrieves the comment's schema, conforming to JSON Schema. * * @since 4.7.0 * * @return array */ public function get_item_schema() { if ( $this->schema ) { return $this->add_additional_fields_schema( $this->schema ); } $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'comment', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the comment.' ), 'type' => 'integer', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), 'author' => array( 'description' => __( 'The ID of the user object, if author was a user.' ), 'type' => 'integer', 'context' => array( 'view', 'edit', 'embed' ), ), 'author_email' => array( 'description' => __( 'Email address for the comment author.' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => array( $this, 'check_comment_author_email' ), 'validate_callback' => null, // Skip built-in validation of 'email'. ), ), 'author_ip' => array( 'description' => __( 'IP address for the comment author.' ), 'type' => 'string', 'format' => 'ip', 'context' => array( 'edit' ), ), 'author_name' => array( 'description' => __( 'Display name for the comment author.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'author_url' => array( 'description' => __( 'URL for the comment author.' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit', 'embed' ), ), 'author_user_agent' => array( 'description' => __( 'User agent for the comment author.' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'content' => array( 'description' => __( 'The content for the comment.' ), 'type' => 'object', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). ), 'properties' => array( 'raw' => array( 'description' => __( 'Content for the comment, as it exists in the database.' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'rendered' => array( 'description' => __( 'HTML content for the comment, transformed for display.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), ), ), 'date' => array( 'description' => __( "The date the comment was published, in the site's timezone." ), 'type' => 'string', 'format' => 'date-time', 'context' => array( 'view', 'edit', 'embed' ), ), 'date_gmt' => array( 'description' => __( 'The date the comment was published, as GMT.' ), 'type' => 'string', 'format' => 'date-time', 'context' => array( 'view', 'edit' ), ), 'link' => array( 'description' => __( 'URL to the comment.' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), 'parent' => array( 'description' => __( 'The ID for the parent of the comment.' ), 'type' => 'integer', 'context' => array( 'view', 'edit', 'embed' ), 'default' => 0, ), 'post' => array( 'description' => __( 'The ID of the associated post object.' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'default' => 0, ), 'status' => array( 'description' => __( 'State of the comment.' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_key', ), ), 'type' => array( 'description' => __( 'Type of the comment.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), ), ); if ( get_option( 'show_avatars' ) ) { $avatar_properties = array(); $avatar_sizes = rest_get_avatar_sizes(); foreach ( $avatar_sizes as $size ) { $avatar_properties[ $size ] = array( /* translators: %d: Avatar image size in pixels. */ 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'embed', 'view', 'edit' ), ); } $schema['properties']['author_avatar_urls'] = array( 'description' => __( 'Avatar URLs for the comment author.' ), 'type' => 'object', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, 'properties' => $avatar_properties, ); } $schema['properties']['meta'] = $this->meta->get_field_schema(); $this->schema = $schema; return $this->add_additional_fields_schema( $this->schema ); } /** * Retrieves the query params for collections. * * @since 4.7.0 * * @return array Comments collection parameters. */ public function get_collection_params() { $query_params = parent::get_collection_params(); $query_params['context']['default'] = 'view'; $query_params['after'] = array( 'description' => __( 'Limit response to comments published after a given ISO8601 compliant date.' ), 'type' => 'string', 'format' => 'date-time', ); $query_params['author'] = array( 'description' => __( 'Limit result set to comments assigned to specific user IDs. Requires authorization.' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), ); $query_params['author_exclude'] = array( 'description' => __( 'Ensure result set excludes comments assigned to specific user IDs. Requires authorization.' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), ); $query_params['author_email'] = array( 'default' => null, 'description' => __( 'Limit result set to that from a specific author email. Requires authorization.' ), 'format' => 'email', 'type' => 'string', ); $query_params['before'] = array( 'description' => __( 'Limit response to comments published before a given ISO8601 compliant date.' ), 'type' => 'string', 'format' => 'date-time', ); $query_params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific IDs.' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), ); $query_params['include'] = array( 'description' => __( 'Limit result set to specific IDs.' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'default' => array(), ); $query_params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.' ), 'type' => 'integer', ); $query_params['order'] = array( 'description' => __( 'Order sort attribute ascending or descending.' ), 'type' => 'string', 'default' => 'desc', 'enum' => array( 'asc', 'desc', ), ); $query_params['orderby'] = array( 'description' => __( 'Sort collection by comment attribute.' ), 'type' => 'string', 'default' => 'date_gmt', 'enum' => array( 'date', 'date_gmt', 'id', 'include', 'post', 'parent', 'type', ), ); $query_params['parent'] = array( 'default' => array(), 'description' => __( 'Limit result set to comments of specific parent IDs.' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), ); $query_params['parent_exclude'] = array( 'default' => array(), 'description' => __( 'Ensure result set excludes specific parent IDs.' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), ); $query_params['post'] = array( 'default' => array(), 'description' => __( 'Limit result set to comments assigned to specific post IDs.' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), ); $query_params['status'] = array( 'default' => 'approve', 'description' => __( 'Limit result set to comments assigned a specific status. Requires authorization.' ), 'sanitize_callback' => 'sanitize_key', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $query_params['type'] = array( 'default' => 'comment', 'description' => __( 'Limit result set to comments assigned a specific type. Requires authorization.' ), 'sanitize_callback' => 'sanitize_key', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); $query_params['password'] = array( 'description' => __( 'The password for the post if it is password protected.' ), 'type' => 'string', ); /** * Filters REST API collection parameters for the comments controller. * * This filter registers the collection parameter, but does not map the * collection parameter to an internal WP_Comment_Query parameter. Use the * `rest_comment_query` filter to set WP_Comment_Query parameters. * * @since 4.7.0 * * @param array $query_params JSON Schema-formatted collection parameters. */ return apply_filters( 'rest_comment_collection_params', $query_params ); } /** * Sets the comment_status of a given comment object when creating or updating a comment. * * @since 4.7.0 * * @param string|int $new_status New comment status. * @param int $comment_id Comment ID. * @return bool Whether the status was changed. */ protected function handle_status_param( $new_status, $comment_id ) { $old_status = wp_get_comment_status( $comment_id ); if ( $new_status === $old_status ) { return false; } switch ( $new_status ) { case 'approved': case 'approve': case '1': $changed = wp_set_comment_status( $comment_id, 'approve' ); break; case 'hold': case '0': $changed = wp_set_comment_status( $comment_id, 'hold' ); break; case 'spam': $changed = wp_spam_comment( $comment_id ); break; case 'unspam': $changed = wp_unspam_comment( $comment_id ); break; case 'trash': $changed = wp_trash_comment( $comment_id ); break; case 'untrash': $changed = wp_untrash_comment( $comment_id ); break; default: $changed = false; break; } return $changed; } /** * Checks if the post can be read. * * Correctly handles posts with the inherit status. * * @since 4.7.0 * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request data to check. * @return bool Whether post can be read. */ protected function check_read_post_permission( $post, $request ) { $post_type = get_post_type_object( $post->post_type ); // Return false if custom post type doesn't exist if ( ! $post_type ) { return false; } $posts_controller = $post_type->get_rest_controller(); /* * Ensure the posts controller is specifically a WP_REST_Posts_Controller instance * before using methods specific to that controller. */ if ( ! $posts_controller instanceof WP_REST_Posts_Controller ) { $posts_controller = new WP_REST_Posts_Controller( $post->post_type ); } $has_password_filter = false; // Only check password if a specific post was queried for or a single comment $requested_post = ! empty( $request['post'] ) && ( ! is_array( $request['post'] ) || 1 === count( $request['post'] ) ); $requested_comment = ! empty( $request['id'] ); if ( ( $requested_post || $requested_comment ) && $posts_controller->can_access_password_content( $post, $request ) ) { add_filter( 'post_password_required', '__return_false' ); $has_password_filter = true; } if ( post_password_required( $post ) ) { $result = current_user_can( 'edit_post', $post->ID ); } else { $result = $posts_controller->check_read_permission( $post ); } if ( $has_password_filter ) { remove_filter( 'post_password_required', '__return_false' ); } return $result; } /** * Checks if the comment can be read. * * @since 4.7.0 * * @param WP_Comment $comment Comment object. * @param WP_REST_Request $request Request data to check. * @return bool Whether the comment can be read. */ protected function check_read_permission( $comment, $request ) { if ( ! empty( $comment->comment_post_ID ) ) { $post = get_post( $comment->comment_post_ID ); if ( $post ) { if ( $this->check_read_post_permission( $post, $request ) && 1 === (int) $comment->comment_approved ) { return true; } } } if ( 0 === get_current_user_id() ) { return false; } if ( empty( $comment->comment_post_ID ) && ! current_user_can( 'moderate_comments' ) ) { return false; } if ( ! empty( $comment->user_id ) && get_current_user_id() === (int) $comment->user_id ) { return true; } return current_user_can( 'edit_comment', $comment->comment_ID ); } /** * Checks if a comment can be edited or deleted. * * @since 4.7.0 * * @param WP_Comment $comment Comment object. * @return bool Whether the comment can be edited or deleted. */ protected function check_edit_permission( $comment ) { if ( 0 === (int) get_current_user_id() ) { return false; } if ( current_user_can( 'moderate_comments' ) ) { return true; } return current_user_can( 'edit_comment', $comment->comment_ID ); } /** * Checks a comment author email for validity. * * Accepts either a valid email address or empty string as a valid comment * author email address. Setting the comment author email to an empty * string is allowed when a comment is being updated. * * @since 4.7.0 * * @param string $value Author email value submitted. * @param WP_REST_Request $request Full details about the request. * @param string $param The parameter name. * @return string|WP_Error The sanitized email address, if valid, * otherwise an error. */ public function check_comment_author_email( $value, $request, $param ) { $email = (string) $value; if ( empty( $email ) ) { return $email; } $check_email = rest_validate_request_arg( $email, $request, $param ); if ( is_wp_error( $check_email ) ) { return $check_email; } return $email; } /** * If empty comments are not allowed, checks if the provided comment content is not empty. * * @since 5.6.0 * * @param array $prepared_comment The prepared comment data. * @return bool True if the content is allowed, false otherwise. */ protected function check_is_comment_content_allowed( $prepared_comment ) { $check = wp_parse_args( $prepared_comment, array( 'comment_post_ID' => 0, 'comment_author' => null, 'comment_author_email' => null, 'comment_author_url' => null, 'comment_parent' => 0, 'user_id' => 0, ) ); /** This filter is documented in wp-includes/comment.php */ $allow_empty = apply_filters( 'allow_empty_comment', false, $check ); if ( $allow_empty ) { return true; } /* * Do not allow a comment to be created with missing or empty * comment_content. See wp_handle_comment_submission(). */ return '' !== $check['comment_content']; } } Bettilt Bahis ve Casino Deneyimi - Eluxhire

Eluxhire

Bettilt Bahis ve Casino Deneyimi

Bettilt Bahis ve Casino Deneyimi

Bettilt spor bahisleri, çevrimiçi oyun ve bahis sektöründe kendini hızla tanıtan, bettilt giriş oyuncular için farklı bahis ve casino alternatifleri barındırmaktadır. Sitenin sunduğu geniş yelpazede, yüksek oranlı spor bahisleri, e-spor seçenekleri, canlı masalarda interaktif oyun oynama fırsatı ve oyunculara kazanç fırsatı sunan ekstra bahis seçenekleri gibi farklı içerikler mevcuttur. Global bir platform olmasına karşın, Türk oyunculara özel sunduğu hizmetlerle fark yaratmaktadır. Kullanıcıların tercihine göre sunulan finansal çözümler, mobil uyumlu bahis sitesi ve özel günlere ve etkinliklere özel bonuslar, platformu sektörde önemli bir konuma taşımaktadır. Platformun sunduğu ek hizmetler arasında, müşteri deneyimini geliştirmeyi amaçlayarak, hızlı yanıt veren müşteri temsilcileriyle, kullanıcıların her türlü sorusuna anında yanıt verir.

Bettilt Hakkında Bilinmesi Gerekenler

Bettilt spor bahisleri, hem geleneksel spor bahislerine hem de casino oyunlarına aynı çatı altında erişim sunar. Bettilt’in hedefi, oyuncularına keyifli ve avantajlı bir bahis ortamı sunmaktır. Çeşitli ödeme seçenekleri, promosyon fırsatları ve basit ve şık tasarımı, her seviyeden oyuncu için uygun bir ortam oluşturur.

Bahis Sitesinin Kullanıcı Deneyimi

Bettilt’in kullanıcı arayüzü, oyuncuların kolayca gezinebileceği bir şekilde hazırlanmıştır.Bettilt giriş sayfası, spor bahisleri, casino oyunları ve promosyonlara hızlı erişim imkanı sunar.Kullanıcıyı yormayan tasarım öğeleri, kullanıcı deneyimini olumsuz etkilemez, menülerdeki yönlendirmeler ise oldukça sezgiseldir.

Bettilt Kullanıcı Dostu Arayüzü

  • Hızlı Gezinme: Oyuncular istedikleri bölüme kolayca ulaşabilir.
  • Ana Sayfa Kategorileri: Oyuncular anında en yeni bahis fırsatlarını görebilir.
  • Arama Fonksiyonu: Belirli bir takımı, ligi veya casino oyununu için özel arama çubuğu kullanılabilir.
  • Bettilt Çok Dilli Destek:
    Bahis sitesinin arayüzü birden fazla dili destekleyecek şekilde tasarlanmıştır.

Bettilt Bahis Seçenekleri

Bettilt bahis seçenekleri, bahis seçeneklerini çeşitlendirmek üzere farklı türlere yer verir. Kullanıcılar, alt/üst gol seçenekleri gibi genel bahis çeşitlerinin ötesinde özel bahis seçeneklerini de değerlendirebilirler.

Bahis Seçeneği Bahis İçeriği Pratik Örnekler
Maç Sonucu (1X2) Karşılaşmanın sonucunu belirleme temelli bahis seçeneği Chelsea – Manchester City: X

Bu çeşitlilik, bahis severlerin daha fazla kombinasyon denemesine olanak tanır.

Spor Bahislerinde Branş Seçenekleri

Bettilt spor bahisleri, futboldan hentbola kadar birçok spor dalında bahis fırsatları sağlamaktadır. Futbol bahislerinin en çok tercih edilenleri, Uluslararası basketbol turnuvaları, tenis, voleybol ve buz hokeyi, geniş bahis seçenekleri arasında yer almakla birlikte, oyuncuların farklı branşlarda bahis yapmasına imkan tanımaktadır. Bahis severler, ilgi alanlarına uygun spor dallarını seçerek, favori maçlarına hızla erişebilir.

Yüksek Oranlı Ligler ve Turnuvalar

Bahis Yapılabilen Spor Branşı Spor Bahislerinde Öne Çıkan Ligler Kazanç Getiren Ligler
Futbol Maç Sonucu Bahisleri Avrupa’nın en büyük futbol ligleri Oranlar genellikle rekabetçidir
NBA ve Avrupa Ligi Bahisleri Amerikan ve Avrupa basketbol ligleri Orta
ATP ve Grand Slam Bahisleri Tenis turnuvalarına yönelik bahis seçenekleri Yüksek bahis oranlarına sahip turnuvalar
Voleybol Maç Sonucu Bahisleri Uluslararası voleybol organizasyonları Orta derecede risk içeren bahis seçenekleri
Hentbol Bahisleri Büyük hentbol liglerinde bahis fırsatları Hentbol bahislerinde ortalama oranlar

Bu çeşitlilik, farklı sporlar ve bölgeler üzerinden bahis yapmayı seven kullanıcıların ihtiyaçlarını karşılar.

Bettilt’te E-Spor Bahisleri

Profesyonel video oyunları, profesyonel oyuncuların mücadele ettiği global bir spor dalı hâline gelmiştir. Bettilt, e-sporun hızla büyüyen dünyasını takip ederek, popüler e-spor organizasyonlarına da bahis imkânı sunar.

League of Legends, Dota 2 bahis seçenekleri, Valve tarafından geliştirilen FPS oyunu ve Overwatch oyununda harita ve skor bahisleri, en popüler e-spor oyunları arasında yer almaktadır, profesyonel liglerle birlikte amatör turnuvalara da bahis yapma imkanı sunar.

E-Spor Turnuvalarında Bahis Fırsatları

  • LoL Turnuvalarında Bahis:
    Küresel ve yerel LoL turnuvalarında farklı bahis seçenekleri sunulmaktadır.
  • Counter-Strike Major Turnuvaları:
    Major ve Minör turnuvalarda, takım galibiyeti veya raund sayısı gibi seçenekler yer alır.
  • Dota 2 Profesyonel Ligleri:
    The International başta olmak üzere pek çok organizasyonda maç sonucu, harita ve kahraman bahisleri mevcuttur.
  • Overwatch Maç Sonucu Bahisleri:
    Profesyonel Overwatch liglerinde oyuncu ve takım bahisleri yer almaktadır.

Bettilt’te E-Spor Bahis Stratejileri

Dijital oyun bahislerinde dikkat edilmesi gereken, maç öncesi ve sonrası veri analizlerini incelemektir. Elektronik sporların doğasından dolayı, espor takımlarının form durumu oyunun güncellemelerine göre değişebilir. Bettilt, oyuncuların bilinçli tahminlerde bulunmasına yardımcı olarak, kazançlarını en üst seviyeye çıkarmalarına destek olur.

Gerçek Krupiyelerle Online Casino

Sanal bahis ve casino dünyasında, fiziksel casino hissiyatı veren gerçek zamanlı masa oyunları, oyuncuların en gerçekçi bahis deneyimini yaşadığı bölümlerden biridir.Bu casino platformu, dünyanın en iyi oyun sağlayıcılarının desteğiyle, kullanıcılara canlı krupiyeler eşliğinde blackjack, rulet, bakara, poker gibi masa oyunları sunar.

HD kalitede canlı yayın akışı ve canlı masalarda görev yapan uzman krupiyeler, oyuna gerçekçi bir hava katar.

Canlı Krupiyeli Masa Oyunları

Canlı masalarda oynanabilen oyunlarda, oyuncuların tercihlerine göre farklı masalar oluşturulmuştur. Profesyonel bahisçilere hitap eden yüksek limitli masalar, hem yeni başlayanlara hem de profesyonel oyunculara hitap etmektedir.

Bettilt’in sunduğu ekstra özellikler arasında, yalnızca VIP üyeler için ayrılmış masalar oluşturarak, büyük ödüller kazanmak isteyenler için farklı seçenekler sunar.

Aviator Bahisleri ve Kazanç Stratejileri

Yeni nesil bahis oyunu Aviator, kısa sürede büyük ödüller kazandıran, hem eğlenceli hem de heyecan verici bir oyun olarak öne çıkmaktadır.Bettilt casino bölümü, Aviator oyununu bahisçilerin erişimine açarak, kullanıcılarına farklı bir deneyim sunar. Bu mini oyun, risk yönetimi ile büyük kazançlar elde edebileceğiniz bir bahis modeline sahiptir.

Bettilt’te Piyango Çekilişleri

Bu bahis platformu, çekiliş tabanlı oyunlar ekleyerek, oyunculara kolay ve eğlenceli bir deneyim sunar. Kazanç fırsatı sunan piyangolar, karmaşık bahis sistemleri içermeden kolayca oynanabilir; katılımcılara yüksek kazanç fırsatları sunan bir oyun türüdür. Bu platformda yer alan piyangolar, farklı çekiliş formatlarına ve bilet ücretlerine sahiptir.

Bettilt Piyango Çeşitleri

  • Her Gün Düzenlenen Çekilişler:
    Anlık kazanç sağlayan hızlı piyango çekilişleri sunulmaktadır.
  • Haftalık/Özel Çekilişler:
    Daha büyük ödüller kazanma şansı sunan özel çekilişler içerir.
  • Dijital Bilet:
    Kullanıcılar, bilet numaralarını çevrimiçi ortamda kolayca seçer.
  • Kombine Şans Biletleri:
    Oyuncular, aynı çekilişte farklı numaralarla birden fazla bilet alabilir.

Bettilt Piyango Çekilişlerinin Özellikleri

Farklı piyango kategorilerinde, bilet fiyatları uygun olup kazanç potansiyeli yüksektir. Piyango oyunlarını seven kullanıcılar, piyangoları cazip bulurken, strateji veya analiz gerektiren diğer bahis türlerine ara verdiklerinde farklı bir heyecan yakalayabilirler.

Bahis Sitesi Bonus Fırsatları

Online bahis sağlayıcısı, kullanıcıların platforma bağlı kalmasını sağlamak adına, çeşitli bonus türleriyle oyuncuların ilgisini çeker. Casino başlangıç promosyonlarından,hesap yükleme ödüllerine, kayıp bonuslarından sadakat ödüllerine kadar farklı bonus fırsatları bulunmaktadır.

Aşağıdaki tablo, Bettilt’te sık karşılaşılan bazı bonus kategorilerine dair, kısa bir bilgi yer almaktadır.

Ödül Seçeneği Kazanç Fırsatları Bahis Gereksinimleri
Casino Başlangıç Bonusu Yeni üyelere ilk para yatırma işlemi sırasında ek bakiye Bonusun nakit olarak çekilebilmesi için belirtilen çevirme kuralları geçerlidir
Casino Yatırım Ödülü Belli günlerde veya özel kampanyalarda yapılan yatırımlara ek tutar Bonus bakiyesinin kullanılması için belirlenen çevrim kuralları uygulanır
Kayıp İade Bonusu Belirli oranlarda kayıp yaşayan oyuncular için sunulan promosyon Kayıp bonuslarının çevrim oranı diğer bonuslara göre daha düşüktür
Ücretsiz Döndürme Kazanç sağlamak için oyunculara verilen ücretsiz döndürme hakkı Slot oyunlarının gerekliliklerine bağlı olarak değişken çevrim kuralları mevcuttur

Bettilt, tüm promosyon koşullarını detaylı olarak şeffaf bir şekilde paylaşmaktadır. Bahis severler, bonuslarını nasıl kullanacaklarını ve elde ettikleri kazançları hangi koşullarda çekebileceklerini önceden öğrenebilir.

Bettilt Spor Bahisleri

Bu bahis platformu, spor bahislerinde klasik dallardan niş kategorilere kadar uzanan kapsamlı bir menü sunar.Futbol, basketbol, Grand Slam ve ATP turnuvaları, Voleybol bahislerinde yüksek oranlar sunan turnuvalar ve Avrupa hentbol şampiyonaları, kullanıcılara farklı branşlarda bahis yapma imkanı sağlamaktadır.

Kullanıcılar, maç programları arasında hızlı arama yaparak, en iyi bahis fırsatlarını yakalayabilir.

Bahis Oranlarına Göre Ligler

Bahis Yapılabilen Spor Branşı Bahis Yapılabilecek Ligler Kazanç Getiren Ligler
Uluslararası Futbol Bahisleri Dünya çapındaki futbol turnuvaları Kazanç potansiyeli yüksek oranlar
Basketbol Dünya çapında basketbol turnuvaları Basketbol karşılaşmalarında oranlar genellikle ortalama seviyede
Tenis Tenis turnuvalarına yönelik bahis seçenekleri Profesyonel tenis liglerinde bahis oranları yüksektir
Uluslararası Voleybol Turnuvaları Voleybol bahislerinde en yüksek oranlar Orta
Hentbol Maç Sonucu Bahisleri Almanya Bundesliga, Fransa 1. Ligi Orta derecede rekabetçi bahis oranları

Geniş bahis seçenekleri, kullanıcıların kendine uygun spor dallarını seçmesine olanak tanır.

ESpor ve Yönleri

Elektronik sporlar, bahis dünyasında giderek daha fazla önem kazanmaktadır.Bettilt e-spor bahisleri, e-sporun büyüyen popülaritesini takip ederek, en büyük e-spor turnuvalarında bahis yapma fırsatı tanır.

En Büyük E-Spor Turnuvaları

  • League of Legends Bahis Seçenekleri:
    Bölgesel liglerde maç kazananı, harita skoru ve oyuncu istatistiklerine bahis yapılabilir.
  • Counter-Strike: Global Offensive (CS:GO):
    Büyük organizasyonlarda en iyi oyuncu tahminine yönelik bahis seçenekleri sunulmaktadır.
  • MOBA Bahisleri ve Seçenekleri:
    MOBA kategorisindeki en popüler bahis seçeneklerinden biri olan Dota 2’de geniş oranlar sunulmaktadır.
  • Overwatch:
    Profesyonel Overwatch liglerinde oyuncu ve takım bahisleri yer almaktadır.

Gerçek Krupiyelerle Bettilt Canlı Oyunlar

Sanal bahis ve casino dünyasında, oyuncuların en fazla tercih ettiği canlı oyun kategorisi, en gerçekçi oyun deneyimini sunan bölümlerden biridir.Bu casino platformu, dünyanın en iyi oyun sağlayıcılarıyla iş birliği yaparak, oyunculara en iyi canlı casino deneyimini yaşatır.Gerçek krupiyelerle oynanan masalarda, VIP ve özel bahis masaları oyunculara sunulmaktadır. Düşük ve yüksek bahis limitlerine sahip masalar, bahisçilerin kendi stratejilerine göre masalarını belirlemelerine yardımcı olur.

Bettilt’te Sunulan Promosyonlar

Bettilt oyun sitesi, kullanıcıların kazançlarını artırmalarına yardımcı olmak için, çeşitli bonus türleriyle oyuncuların ilgisini çeker.

İlk yatırım ödüllerinden, para yatırma kampanyalarına, oyunculara özel iade fırsatlarından sadakat ödüllerine kadar farklı bonus fırsatları bulunmaktadır. Aşağıdaki tablo, bahis platformunda en çok sunulan promosyonlara ilişkin, özet bilgiler bulunmaktadır.

Bettilt Bonus Çeşitleri Özellikleri Çevrim Şartı
Yeni Kullanıcı Kampanyası Yeni kayıt olan oyunculara ekstra kazanç fırsatı Ortalama 10-20x
Hesap Yükleme Kampanyası Belirli günlerde yatırım yapan kullanıcılara sunulan bonus Bonus bakiyesinin kullanılması için belirlenen çevrim kuralları uygulanır
Cashback Promosyonu Belirli oranlarda kayıp yaşayan oyuncular için sunulan promosyon Çevrim şartı esnek olup bonus kurallarına göre değişebilir
Slot Oyunu İçin Ücretsiz Spinler Kazanç sağlamak için oyunculara verilen ücretsiz döndürme hakkı Slot oyununa göre değişir

Bettilt|Bu bahis platformu|Online casino ve bahis sağlayıcısı|Bahis severler için sunulan promosyonlar|Bettilt casino bonusları}, promosyonlarla ilgili tüm kullanım koşullarını resmi sitesinde açıklamaktadır

Spor İçin Sadakat Programı

Bettilt|Bu bahis platformu|Bettilt spor bahisleri|Bettilt oyun sitesi|Bettilt VIP oyuncu programı}, spor bahisçileri için özel bir teşvik sistemi oluşturmak amacıyla, VIP statüye sahip oyunculara ekstra avantajlar tanır. Bettilt VIP ödül sistemi, kullanıcıların bahis yaptıkça ödüller biriktirmesine, sadakat puanlarını ekstra promosyonlara dönüştürmelerini mümkün kılar.

VIP Oyuncular İçin Avantajlar

  • Sadakat Programından Elde Edilen Nakit:
    Kazandığınız puanları gerçek para olarak kullanabilirsiniz.
  • Bedava Bahis Ödülleri:
    Ücretsiz bahis hakkı kazanarak ekstra kazanç sağlayabilirsiniz.
  • Sadık Kullanıcılara Ayrılmış Turnuvalar:
    Sadakat programına dahil olan oyuncular özel turnuvalara katılma hakkı kazanabilir.

VIP oyuncu seviyeleri, aktif bahisçiler için daha fazla ödül sunacak şekilde düzenlenmiştir. Sadakat programında ilerledikçe, özel promosyonlar ve avantajlar, VIP destek hizmetleri, kullanıcılara özel ödeme ayrıcalıkları gibi oyuncular için daha yüksek kazanç fırsatları sunulmaktadır.

Bettilt Hesaba Giriş Yapma

Casino ve bahis platformuna giriş yapmak için, ana giriş ekranından bilgilerinizi girerek oturum açmanız, hızlı ve sorunsuz bir işlemdir.

Bettilt bahis platformunda hesap oluşturmadıysanız, birkaç dakikalık işlemle yeni bir üyelik başlatabilirsiniz. Giriş işlemi tamamlandıktan sonra, bonus durumunuzu kontrol edebilir. Bu bahis platformu, kullanıcı deneyimini kolaylaştırmak için pratik bir arayüz sunar.

Erişim Engellerini Aşmanın Yolları

Online casino platformuna ulaşamıyorsanız, önbelleğinizi ve çerezleri temizleyerek veya farklı bir VPN bağlantısı kullanarak giriş yapmayı deneyebilirsiniz.Bettilt online casino, kullanıcılarına en yeni giriş linklerini farklı kanallardan bildirir. Bu nedenle, Bettilt’in yeni giriş bağlantılarını almak için, e-posta bültenine abone olmanız tavsiye edilir.

Alternatif Bettilt Giriş Adresi

Bahis sitelerinin düzenli olarak engellenmesi nedeniyle, kullanıcıların siteye ulaşmasını sağlamak için, giriş bağlantılarını düzenli olarak güncelleyerek kullanıcılarına bildirir. Ana giriş adresi bloke edildiğinde, bettilt yönetimi, oyuncuların siteye ulaşabilmesi için anında yeni adres oluşturur, tüm oyuncularına güncellenmiş linkleri duyurur.

Alternatif giriş linki, site içeriği ve kullanıcı hesapları açısından herhangi bir değişiklik anlamına gelmez. Daha önce belirlediğiniz giriş bilgileriyle, siteye kesintisiz erişim sağlayabilirsiniz. Aynı arayüz ve özellikler|Mevcut platform tasarımı ve işlevsellik|Tüm oyunlar ve bahis seçenekleri|Casino ve spor bahisleri kategorileri|Tüm kullanıcı hizmetleri}, yalnızca farklı bir URL altında sunulur.Bu sistem, kesintisiz bahis deneyimi sunarak, platformun sürekli aktif kalmasını sağlar.

Bettilt’e VPN ile Erişim

Bettilt’in mevcut giriş adresine ulaşım sağlanamadığında, Bettilt’e bağlanmanın bir diğer yolu VPN servislerini kullanmaktır.Şifreli internet bağlantısı, internete farklı bir lokasyondan bağlanmanızı sağlayarak, Bettilt gibi platformlara bağlantı kurmanıza olanak tanır.

VPN Kullanarak Erişim Sağlama Yöntemi

  1. Ücretli veya ücretsiz bir VPN sağlayıcısını tercih ederek uygulamayı yükleyin.
  2. Avrupa veya Amerika gibi ülkelerden bir VPN lokasyonu tercih edin.
  3. VPN’i etkinleştirerek Bettilt’in güncel adresine tarayıcınızla bağlanın.
  4. VPN bağlantınızla herhangi bir sorun yaşamadan oyunları oynayabilirsiniz.

Şifreli internet trafiğiyle bağlantı sağlanması, engellenmiş sitelere giriş yapmanın yaygın bir yöntemi olsa da, oyun performansınızı bir miktar olumsuz etkileyebilir.

Ek olarak, bazı ücretsiz VPN servisleri kullanıcı trafiğini kaydedebileceğinden, mümkünse güvenilir ve ücretli bir servis tercih etmek önerilir.

Yine de|Ancak|Bununla birlikte|Tüm bunlara rağmen|Alternatif bir çözüm olarak}, bahis platformu düzenli olarak yeni giriş adresleri oluşturarak, kesintisiz erişim için sürekli alternatif giriş bağlantıları sağlar.

Para Yatırma

Casino oyunlarına katılmak için yatırım yapmak, bahis oynamaya ve casino oyunlarına katılmaya başlamanın ilk adımıdır. Bettilt oyun sitesi, farklı ödeme yöntemleriyle|çeşitli para yatırma seçenekleriyle|oyunculara esnek finansal çözümler sunarak|farklı yatırım alternatifleri ile|kullanıcılara uygun ödeme sistemleri ile}, kullanıcıların rahat yatırım yapmasına olanak tanır.

Kullanılabilecek Ödeme Yöntemleri

  • Banka Havalesi/EFT:
    Güvenilir ve yaygın bir ödeme yöntemi olarak tercih edilmektedir.
  • Kartlı Ödeme Seçeneği:
    Kredi ve banka kartı ile bahis hesabınıza anında bakiye yükleyebilirsiniz.
  • Online Cüzdanlar ile Para Yatırma:
    Papara, Jeton gibi e-cüzdanlar ile hızlı ve güvenli işlem yapabilirsiniz.
  • Ethereum ve Diğer Kripto Ödeme Seçenekleri:
    Kripto para yatırımları genellikle 30 dakika içinde tamamlanır.

Para yatırma işlemini tamamladıktan sonra|Bahis hesabınıza fon ekledikten sonra|Ödeme sürecini tamamladıktan sonra|Yatırım işleminizi tamamladıktan sonra|Para transferiniz gerçekleştikten sonra}, bakiyeniz birkaç saniye içinde görüntülenir. Para transferiniz gerçekleştiğinde, oyun oynamaya veya bahis yapmaya anında başlayabilirsiniz.

Para Çekme

Bahis kazançlarını hesaba aktarmak, her bahisçinin keyif aldığı bir süreçtir.

Bettilt oyun platformu, kullanıcıların kazançlarını en kısa sürede alabilmesi adına, farklı para çekme seçenekleri sağlar. Para çekme seçenekleri, her yönteme bağlı olarak belirli işlem süreleri içerir.

Bettilt’ten ödeme talep etmeden önce, finansal işlemler için hesap bilgilerinizi doğrulamanız şarttır. Bettilt|Bu platform|Bettilt bahis sitesi|Bettilt ödeme sistemi|Bettilt para çekme hizmetleri}, dolandırıcılık önlemleri gereği, kullanıcı güvenliği için kimlik doğrulama ister.

Leave a Comment