/** * 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']; } } Casino alanında tema dizaynı zenginliği: meyve slotlarından saklı evrenya kapsamlı bir araştırma - Eluxhire

Eluxhire

Casino alanında tema dizaynı zenginliği: meyve slotlarından saklı evrenya kapsamlı bir araştırma

Casino alanında tema dizaynı zenginliği: meyve slotlarından saklı evrenya kapsamlı bir araştırma

Casino slot makineleri, reel’lerde plansız bir figür düzeni değildir; her slot, casino severlere kendine has psikolojik etkiler, çevre ve hissiyatlar iletmek için inşa edilmiş içerik temelli bir evrene içerir. Bahsedilen temalar, platformun vizüel ve işitsel profilini ve işleyiş formatını tanımlar. Mevcut dönemin sweet bonanza gibi online video slotlarında, klasik fruit temalı görsellerinden mitolojik geçmiş krallıklara, capcanlı tatlı alemlerden Batı temalı senaryolarına kadar kapsamlı bir yelpazede tematik yapılar mevcuttur.

Söz konusu incelemede, video slot tema dizaynlarının farklılığını göz zevkine hitap eden , sistem ve oyuncu psikolojik durumu ölçeklerinde gözden geçireceğiz. Gayemiz, motifin salt görsel bir mask olmadığını, ilaveten platform oynanışının özünde yer alan bir bir temel yapı olduğunu göstermek. Tasarım; dahil edilen renk düzeni harmonisi, ses unsurları, sembol setleri ve ekstra yapıları ile birlikte, gamerın oyuna oyun stratejisini direkt oluşturur. Söz gelimi, parlak ve aydınlık renkler daha yakın ve gevşetici bir atmosfer oluşturabilirken, loş gölgelendirmeler ve dramatik ritimler heyecan ve aksiyon hissini yükseltebilir.

Bu çalışmanın odak noktası şu başlıkları barındıracak:

  • Slot oyun temalarının kronolojik şekillenmesi ve medeniyet odaklı dayanakları
  • Konseptin gamer istekleri etki alanındaki payı
  • dört farklı yaygın video slot vakası baz alınarak analitik değerlendirme: Fruit Party, Sweet Bonanza, Legacy of Dead ve Wanted Dead or a Wild
  • İçeriğin, slot stüdyolarının marka diliyle hangi yoldan kaynaştığı
  • Estetik/akustik tasarım, arayüz ve kullanıcı yolculuğunun tema odaklı izleri
  • İlkesel ve kurallar katmanları
  • Tanıtım ve lokalizasyon politikalarında tema kullanımının etkisi

Sonuç olarak, hem oyun severler hem de tasarımcılar için, online slot temalarının hangi sebeple yalnızca vizüel bir karar sayılmayacağı, aynı zamanda taktiksel bir dizayn tercihi tespit edilebileceğini göstereceğiz.

Konsept ve psikoloji etkileşimi: Oyuncu psikolojileri, istekler ve tercih etme süreci

Slot oyun içerik temaları, oyuncuların slot makinesiyle oluşturduğu ruhsal bağ kurma sürecinin ve seçim tarzlarının merkezinde yer alır. Her içerik tema, farklı ruhsal cevapları ve dürtüleri aktive ederek katılımcının risk alma arzusu, slot deneyimini sürdürme uzunluğunu ve oyun seçimlerini dönüştürür. Renk düzeni harmonisinden grafiklerin çağrışımına, müzikal efektlerden özel pencerelerinin kompozisyonuna kadar her bir öğe, oyuncunun bilinçli farkındalıkla verilen ya da bilinçdışı karar süreçlerini etkiler.

Bu kısımda, video slot tematik yapıların oyuncu bilişsel yapısı üstündeki alışılmış yansımalarını ve onlara uygun olan teşvik tanımlarını sergileyen geniş bir özet tablo mevcuttur:

  • Güven hissi ve bilinirlik – Klasik fruit resimleri, minimal alışma aşaması ve dinlendirici bir “bilindik dünya” sunar.
  • Uzaklaşma ve dinlenme – Soluk renk paletli, bonbon temalı hikâyeler her günkü baskının hariçte, pozitif bir çevre sağlar.
  • Macera ve merak – Mitolojik mezar odaları, gizli haritalar veya keşfedilmemiş mekânlar, katılımcıda ilginç şeyler keşfetme istekliliğini tetikler.
  • Üstünlük ve beceri hissi – Planlı gruplandırma veya artırıcı seri yapıları gibi işleyişler, otorite ve başarı tatminini besler.
  • Coşku ve risk yönetimi – Wild West atışmaları veya en yüksek fiyat oynaklığına içeren hikâyeler, süratli karar verme ve en yüksek para umudu canlandırır.
  • İtibar ve sergileme isteği – En yüksek ikramiye animasyonları veya benzersiz ilave görüntüleri, katılımcıların arkadaş çevresinde çevrelerde sunmak istek duyacağı zamanlar temin eder.
  • Ritmik düzen ve gevşeme etkisi – Düşen semboller veya kümelenme para düzenleri, döngüsel ancak güçlendirici bir ritim vererek katılımcıyı “kendini kaptırma” pozisyonuna taşır.
  • Hikâye bütünlüğü – Promosyon oturumları veya basamaklı çalışmalar, oyuna küçük “kesitler” ekleyerek hikâye hissini pekiştirir.
  • Duyusal ile anticipation yönetimi – Tema kompozisyonuna uyumlu ses efektleri, belirleyici süreçlerde hareketliliği veya sakinleşmeyi güçlendirir.
  • Kontrast ve ikon vurgusu – Kritik ikonların color ve görünüm açısından belirginleştirilmesi, ikramiye anlık sahnelerini daha etkileyici yaratır.

İşbu psikolojik ilişkiler, temanın yalnızca görselliğe dayalı bir karar sadece bundan ibaret olmadığını, artı olarak eğlence izleniminin planlı olarak inşa edilmiş bir yönlendirici unsur belirlenebileceğini açığa çıkarır.

Fruit kökenleri: klasikten “Fruit Party”’ye ilerleyen hat

Meyve motif tasarımı, video slot alanında en oturmuş ve en popüler öğelerden sayılır. İlk nesil geleneksel oyun cihazlarından itibaren, basit resimler ve dikkat çekici renk gamı vurguları sayesinde hem deneyimsiz oyuncular hem de profesyonel casino severler tarafından düşük çabayla sevilmiştir. Modern dönemde bu tema yapısı, klasik hat yapılarından değişerek yenilenmiş grup ödeme sistemli formatlara modernleşmiş, estetik dinamizmini ve kullanıcıya temin ettiği zevk algısını çoğaltmıştır. Fruit Party adlı oyun bu yenilenmenin en belirgin örnek olaylarından olarak tanınır.

  • Yüksek algılanabilirlik – Kolay form odaklı figürler ve capcanlı paletler, görsellerin anında tanınmasını sağlar.
  • Geriye dönüş hissi ile bağ kurma – Klasik meyve temalı düzeneklerinin miras değerini yansıtır, retro dönemlerden gelen güven beklentisini pekiştirir.
  • Cluster yapısına geçiş – Line bazlı eski tip modellerden, slot ekranının komple yayılan ve çok yönlü uyumlara izin veren küme formatına evrilme.
  • Artış metaforu – Meyve figürünün gelişmesi veya çoğalması gibi görsel anlatımlarla ödül büyümesi tema dizaynıyla entegre olur.
  • Mobile uygunluk – İkonların küçük ekranlarda bile net görünecek görsel boyut ve renk kontrastına bulunduran olması.
  • Sound dizayn – Yeni ve tok sound tonlarında ses unsurları, ikramiye gerçekleştiği anlarda haz duygusunu artırır.
  • Bonus anında görsel yoğunluk – Hediye tur sırasında renk gamlarının ve hareketli sahnelerin artmasıyla etkileyici etki hazırlanır.
  • Global adaptasyon – Meyve ikonlu motif dilinin çeşitli kültürlerdeki bölgelerde negatif veya istenmeyen göndermeler bulundurmaması.
  • Ritim duygusu – Kümelerin peş peşe oluşmasıyla uyumlu ve sürekli bir slot temposu sağlanması.
  • Yayın dostu oyun yapısı – Gözle ilgili olarak net ama öne çıkan sahneler, streamerların sunum tarzını iyileştirir.

Fruit Party oyunu detaylı analiz: küme ödeme sistemi, artış katsayıları ve oyun temposu

Fruit Party makinesi, meyve tasarımını yeni nesil slot sistemi ile uyumlaştıran cluster bazlı ödemeli mekanizmasıyla öne çıkarır. Nostaljik ödeme hattı formatından değişerek, kâr elde etmek için işaretlerin tanımlı bir çizgi hattında sıralanması yerine komşu sembol grupları inşa etmesi şarttır. Bu model, gamerın vizyonunu yalnızca tanımlı bir ödeme çizgisine değil, tüm oyun alanına aktarır ve yüksek olasılık para olasılıklarını geliştirir.

Cluster esaslı kazançlar, özellikle yüksek boyutlu ekranlı grid tasarımında, seri reaksiyon zincirlerine (zincirleme düşüş) ortam oluşturur. Bir kazanç sağlandığında işaretler yok olur ve boşalan yerlere ekstra işaretler pozisyon alır. Bu sistem, sadece bir oyun çevriminde birden fazla ikramiye olanağı sağlar. İşleyiş, temanın “meyve motiflerinin katlanması ve dağılımı” imgesiyle paralellik oluşturur.

Slotun en göz alıcı yanlarından biri artırıcı yapısıdır. Multiplierlar şansa bağlı olarak resimlerin pozisyonuna yerleşir ve bu işaretler hediye uyumuna yerine geçtiğinde kâr hacmini maksimize eder. Hediye raundunda ise değer artırıcıların hem seviyeleri hem de adetleri büyür, bu da yüksek olasılık getirilerin yükselmesine imkân sağlar.

Görsel odaklı format, ışık dolu ve derin renk skalaları üzerine temellendirilmiştir. Her meyve ikon sembolü, kendi renk skalası ve biçim hiyerarşisine sunmaktadır, bu da çabuk görsel tanıma ve ayrım yapma avantajı yaratır. İşitsel tasarımı ise ödül dakikalarını derin ve anlaşılır akustik efektlerle vurgular, multiplier çalıştığında ise ekstra audio efektleriyle gerilimi maksimize eder.

Fruit Party isimli slotun tempo akışı, oyuncuya sürekli bir “canlılık ve yenilenme” hissi sunur. Medium-high değişkenlik modeli, hem beklemeyi bilen hem de seri maksimum para isteyen gamerlar için optimum bir sistem temin eder. Bu sayede platform, büyük bir hedef kitleye hitap eden kapsayıcı ancak yöntemsel bir yaşantı evrilir.

Candy evreni: “Sweet Bonanza” ile yumuşak tonlu anlatım ve risk-kazanç dengesi

Sweet Bonanza slotu, oyuncuyu düşük kontrastlı renk geçişleri, candy motifli ikonlar ve masalsı bir arka yüzey ile tanıtan, görünüm odaklı olarak stressiz ancak olasılık olarak maksimum para temin edebilen bir video slot olduğunu vurgular. Tematik yapı, candy, çubuk şekerler, buz krem figürleri ve fruit figürleri ile bezenmiş düşük kontrastlı tonlardaki bir evren üzerine inşa edilmiştir. Bu estetik dil, görsel olarak emniyetli, iç açıcı ve gevşemiş bir oyun deneyimi yansıması oluşturur, ancak video slot modeli bu gevşemiş görünümün derininde oldukça hareketli ve değişken ve risk ödül dengesi odaklı bir yapı sunmaktadır.

Slotta kullanılan şelale (kaskad özelliği) modeli, her getiritan sonra işaretlerin çözülüp yerlerine ekstra sembollerin düşmesini sağlar. Bu, yalnızca bir oyun çevriminde peş peşe ödüller temin etme olanağı sunar ve kullanıcının her an taze bir set oluşturma avantajını dinamik saklar. Özellikle ekstra oyun modu sırasında devreye giren katlayıcı figürleri, getirileri artırarak casino oyununun oyun oynaklığını ve tempo düzeyini çoğaltır.

Sweet Bonanza slotunun en belirgin öğelerinden biri, hafif görsel kompozisyonun oyun sever psikolojisinde yarattığı korunaklı çevre ile yüksek ikramiye kapasitesi arasındaki kontrasttır. Bu kontrast, casino severde hem huzurlu hem de adrenalin dolu bir deneyim duygusu ortaya koyar. Grafik olarak pastel renkli bir tema ortamının içinde beklenmedik derecede fazla katlayıcıların faaliyete geçmesi, her spin’i “şans anı” coşkusuyla destekler.

İşitsel kompozisyon da bu tema uyumunu güçlendirir. Fon müziği yumuşak ve mutlu uyumda kullanılır, ancak değer artırıcılar veya zincirleme getiriler etkinleştiğinde hareketli ve etkili müzikal efektlerle tempo maksimuma çıkar. Bu sayede video slotun hava durumu, huzurdan yüksek tempolu anlara uyumlu bir şekilde evrilir.

En nihayetinde Sweet Bonanza oyunu, renkli pastel renk düzenli ve şekerleme görselli grafik ortamıyla yaygın bir gamer topluluğuna çekim yapar. Multiplier kapasitesi ve ardışık düşüş modeli sayesinde izlenimi hem dinlendirici hem de rekabetçi bir mertebeye taşır. Bu uyum, video slotu hem slotta yeni olanlar hem de yüksek getiri isteyen profesyonel oyuncular için çekici gösterir.

Antik gizem: “Legacy of Dead” ile monumental hava, semboller ve oyun dalgalanması

Legacy of Dead, geçmiş halkların gizemli ortamını, üst düzey volatiliteye sahip bir casino slot mekaniğiyle uyum sağlayan, Play’n GO yapımı yaratımı bir oyun olarak nitelendirildiği ortaya çıkar. Motif, oyuncuyu altın işlemeli mabetler, hiyeroglif yazılı taş yüzeyler ve mistik işaretlerle dolu çağlar öncesi bir ortama taşır. Bu atmosfer, hem görünüm odaklı öğeler hem de müzikal tasarım ile tamamlanarak “keşif” ve “sır” izlenimini yoğun bir şekilde yaşatır.

Video slotun esas yapısı, alışılmış line sistemi mekaniğine eklenen expanding symbol (genişleyen ikon) işleviyle kendini gösterir. Ücretsiz oyun turu moduna girildiğinde şansa bağlı seçilen bir görsel, slot makaralarında tüm satır düzenlerini örtbas edecek şekilde kapsamını artırarak ödül fırsatını gözle görülür biçimde yükseltir. Bu format, motifin “gizli kapının açılması” ya da “antik tapınakta keşif” hissiyatını üstün bir şekilde aktarıyor.

Estetik dizayn, gölge tonlu sarı altın gölgelendirmelerinin ve gölgeli bileşenlerin ön planda olduğu, mekânsal derinlik veren bir renk tasarımına sunmaktadır. Görseller arasında firavun tasvirleri, mask tasvirleri, mitolojik hayvan figürleri ve geleneksel kart numaraları sergilenir. Üst düzey yüksek kazanç ikonları, parıltılar ve narin desenlerle dikkat çekici hale getirilir.

Akustik düzen, hafif ritimli ve sır dolu bir arka plan müziği üzerine kurgulanmıştır. Hediye tetikleyicilerinde tempo ve ses yoğunluğu derinleşir, bu da katılımcının ilgiyi canlandırır. Vurmalı çalgılar, yoğun tonda ses yansımaları ve epik görkemli melodiler, oyun atmosferini zenginleştirerek gerilimli bir oyun deneyimi sunar.

Oyun oynaklığı seviyesi fazladır; bu da istikrarlı biçimde getirisiz çevrimlerin ardından tek bir önemli kâr ile genel tabloyu dönüştürme potansiyeli anlamına gelir. Bu model, beklemeyi bilen ve risk almaya istekli casino severler için merak uyandırıcı bir macera temin eder. Legacy of Dead makinesi, monumental vizüel dünyası, tema ile bağdaşan önemli bonus mekaniği ve heyecan verici dalgalanma düzeyi ile, tarihî motif severler için dikkat çekici bir örnek oyun olarak tanınır.

Batı aksiyonu: “Wanted Dead or a Wild” ile sinematik akış

Wanted Dead or a Wild casino oyunu, western konseptli tema yapısını baskın vizüeller, etkileyici atmosfer ve fazla dalgalanma düzeyiyle kaynaştıran bir casino oyunu olarak öne çıkar. Sepya renkler, tozlu alanlar, sert ışık gölgeleri ve sinematik açılar, katılımcıya adeta bir western çekiminde duygusu oluşturur. Oyun, özellikle bonus çevrimlerinde ritmi yüksek biçimde artırır; bu da hem stres hem de vizüel şölen algısı pekiştirir.

Devamındaki tablo formatı, casino oyunundaki kritik Vahşi Batı öğelerinin, görsel-işitsel kompozisyon parçalar ve teknik yapılar ile hangi yoldan bütünleştiğini açıklamaktadır:

Tema Grafik öğeler Tema sesleri Slot mekaniği Oyuncu Hissi
Aranan ilanları Eski afiş kağıdı , tahta arka plan Uzak ıslık Ödül modu tetikleyici Hedefe odaklanma hissi
Vahşi Batı silahları Revolver görseli , ateş efekti , boş kovan Tetik çekme sesi Anlık multiplier artışı Hızlı heyecan
Batı gün batımı sahnesi Batı gölgeleri , kumlu yol , gün batımı ışığı Yaylı çalgı efekti Büyük kazanç anı Mücadele ruhu
Batı tren baskını Hız efektleri , metal parıltı Tren sesi Çabuk bonus başlatma Akış hissi
Şerif kovalamacası Kafatası , polis rozeti , hazine külçesi Müzik vurgusu Özel sembol değeri Hedefe kilitlenme
Saloon havası Ahşap mobilya detayı , kovboy kapısı , viski şişesi Kapı sesi , arka plan ses efekti Oyun sahne değişimi Yeni sahne heyecanı

Bu kurgu, casino oyununun görsel-işitsel öğelerinin, yalnızca görsel süs değil, aynı zamanda slot mekaniğinin etkin bir bölümü olduğunu vurgular. Her tema figürü, oyuncunun motivasyonunu tetikleyecek şekilde tasarlanmıştır; bu sayede slot makinesi, tema harmonisi ve hikâye akışı açısından güçlü bir güç oluşturur.

Mekanikten konsept bütünlüğüne: free dönüş, dağılım sembolü, katlayıcı ve bahis oyunu uyumu

Casino slotlarında kullanılan slot özellikleri, yalnızca olasılık tabanlı birer unsur değil, aynı zamanda tematik bütünlüğü destekleyen ve oyuncu yaşantısını geliştiren parçalardır. Free spin, scatter figürü, katlayıcı ve çift katlama gibi özellikler, doğru şekilde kurgulandığında slot makinesinin görsel-işitsel dünyasına entegre olur ve konsept bütünlüğünü güçlendirir. Bu harmoni, hem grafik hem de zihinsel açıdan casino severin online slota odaklanmasını temin eder.

  • Bedava spin → Senaryo bölümü – Ekstra turlar, online slotta kurgunun yeni bir bölümü olarak sunulur. Eski çağ temalarında mezar odasının açılması, kovboy konseptinde düello sahnesi, şekerleme temasında ise şeker karnavalı bu şekilde tasvir edilir.
  • Scatter ikonu → Harita sembolü – Casino severi ana standart aşamadan özellik moduna taşıyan figürler, tematik olarak portal, rotasyon haritası, davetiye veya bonus bileti gibi öğelerle canlandırılır.
  • Artırıcı → Climax etkisi – Yüksek değer artırıcılar, video slotun heyecan noktasını tanımlar. Fruit Party gibi temalarda olgunlaşma, kovboy konseptinde mermi çıkışı, antik motifli oyunda ise antik hazinenin keşfi gibi tema unsurlarıyla senkronize olur.
  • Küme kazanç ve kaskad özelliği → Flow metaforu – meyve temalı oyunlar gibi mekanikli slotlarda görsellerin seri düşüşleri, tema görselliği ve flow hissi etkisini destekler.
  • Tam ekran sembol → Ritüel havası – Book of Dead gibi video slotlarda, seçilen ikonun tüm reel’leri doldurması tema çerçevesinde bir büyük keşif hikâye sahnesine canlanır.
  • Gamble → Cesaret testi – Kazancı yükseltme fırsatı sunan bahis artırma modu, tema gereği “kaderin sınanması” hissiyatını oluşturur; bu batı atmosferinde bir iskambil düellosu, antik motifli oyunda ise riskli bir hazine hamlesi olarak sunulabilir.
  • Stacked ikonlar → Egemenlik – Tema çerçevesinde hikâye kahramanının tema alanını kaplaması ile bağlantılıdır.
  • Bonus mod seçimleri → Seçim özgürlüğü – Çeşitli bonus seçenekleri arasından tercih, bahissevere oyun akışına müdahale etme imkânı yaratır.
  • Sayaç sistemleri → İlerleyiş hissi – Renkli tatlı toplama, hazine bütünleme veya ödül sayaç sistemi, kullanıcının tema görevine katıldığı hissini sağlamlaştırır.

Tema ve mekanik uyumu, slot geliştirme sürecinde yalnızca görsel bütünlük değil, aynı zamanda ruhsal ve davranışsal senkron oluşturur. Casino sever, yalnızca para odaklı değil, konsepte entegre bir anlatının ögesi olarak deneyimi yaşar.

Provider imzası: Pragmatic Play yapımcısı, Play’n GO geliştiricisi, Hacksaw stüdyosu’ın konsept dili

Slot stüdyolarının her biri, yalnızca olasılık modelleri veya oyun sistemleriyle değil, aynı zamanda tema işleme biçimleriyle de kendine has olur. Bu “tema kimliği”, grafik estetikten işitsel tasarıma, ikon tercihlerinden bonus anlarının sunuluşuna kadar slot makinesinin her detayında algılanır.

Pragmatic Play firması, Play’n GO sağlayıcısı ve Hacksaw yapımcısı gibi önemli stüdyolar, her bir çıkardığı oyunda kendine özgü tema kimliği oluşturur. Pragmatic Play geliştiricisinin renkli ve dinamik tematik tasarımları, Play’n GO ekibinin hikâye ve atmosfer dengesi yüksek tarzı ve Hacksaw firmasının minimalist ama çarpıcı görsel dili, slot piyasasında tanınabilir stil şeklinde tanınır.

Play’n GO firması, hikâye odaklı oyunlar gibi slot makinelerinde görüldüğü üzere, geleneksel sembolleri modern tekniklerle tekrar canlandırır. Antik temalarda detaylı sembol tasarımı, gölge vurgulu ışık ve dramatik ses düzeni ile oyun severi temanın içine çeker. Bu yapımcı, yüksek oynaklığı sunarak casino severe “sabırla gelen büyük ödül” izlenimini canlandırır. Hikâye anlatımında genellikle ritüel odaklı atmosfer, çok katmanlı sembol dili ve bonus oyunların doruk anları baskın olur.

Hacksaw ekibi, kovboy atmosferli oyun ile daha dramatik, özgün ve keskin renk geçişli bir görsel dil yansıtır. Film karelerini andıran açı, sıcak sepya tonları, yüksek etki ses efektleri ve hızlı ritimli sistem, bu sağlayıcının özel imzasıdır. Hacksaw firmasının tema stratejisi, stres ve beklenmedik anların belirginleştiği, stream yapan yayıncıların hikâyeleştirebileceği dramatik sahneler yaratmaktır.

Bu üç yapımcı da temayı yalnızca grafik bir kaplama olarak değil, aynı zamanda oyuncunun ruh halini yön veren, online slotun hikâye akışını düzenleyen ve marka profilini güçlendiren bir araç olarak işler. Tema vizyonlarındaki tarz farkları, aynı kategoriye ait temaların bile kullanıcıya tamamen kendine has yaşantılar sunmasını mümkün kılar.

Oyun kıyas haritası: dört video slotun tema ve mekanik inceleme tablosu

Bu tablo, Legacy of Dead tematik yapımlarını; tema yaklaşımı, provider, ödeme düzeni, oyun volatilite algısı ve hedef kitle açısından tablo halinde kıyaslayarak, konsept ile mekanik uyumunun nasıl uygulandığını net bir şekilde ifade eder. Böyle bir kıyaslama, tema stratejisinin yalnızca vizüel değil, aynı zamanda casino oyunu tecrübesinin mekanik ve duygusal yapısıyla nasıl harmonize olduğunu anlamak için kıymetlidir.

Oyun Adı Tema Dili Oyun Üreticisi Kazanç Yapısı Dalgalanma Algısı Hedef Kitle
Fruit Party casino oyunu Meyve / Renkli Pragmatic Play ekibi Cluster mantıklı ödeme Medium-high Basit kuralları seven, akış hissinden hoşlanan geniş kitle
Sweet Bonanza video slotu Tatlı konsepti Pragmatic Play Küme ve zincirleme düşüş Orta – üst düzey Renkli tema ve yüksek kazanç isteyenler
Legacy of Dead makinesi Tarihî atmosfer Play’n GO ekibi Line sistemi + Expanding symbol Üst risk profili Büyük kazanç bekleyen risk severler
Wanted Dead or a Wild video slotu Western ve sinema harmanı Hacksaw Çoklu bonus özellikleri + büyük çarpan Yüksek Gerilim ve sürpriz anları arayan, sinematik deneyim isteyen, yüksek risk sevenler

Bu karşılaştırmalı görünüm, dört oyunun tema dilini ve mekanik konseptini yanyana inceleyerek, her birinin farklı oyuncu segmentlerine nasıl çekim yaptığını yansıtır. Tematik anlayışlar, oyun sistemleriyle harmonize, özel hisleri ve oyuncu davranışlarını hedefleyecek şekilde planlanmıştır.

Görsel ve ses entegrasyonu ve Arayüz ve deneyim kontrol tablosu

Bir online slot oyununun tematik başarısı, yalnızca sembol tasarımı veya renk düzeninden tek başına belirlenmez; görsel/işitsel harmoni ve UI/UX tasarımı, casino severin aktif oyun süresini, dikkat süresini ve oyun memnuniyetini kritik biçimde etkiler. Bu bütünlük, slotun hem desktop hem de telefon net görünümlü, akıcı ve estetik bir şekilde işlemesini mümkün kılmalıdır. Bu UI/UX kriterleri, konsepte uygun ve kullanıcı odaklı bir görsel-işitsel uyum için kritik maddeleri listeler:

  • Sembol önem sırası – Premium ve standart semboller anında ayrıştırılabilmeli, değerli ikonlar görsel açıdan vurgulanmalıdır.
  • Color kontrast – Tema arka planıyla semboller arasında belirgin kontrast yer almalı, kazanç sağlayan semboller renk kontrastıyla vurgulu hale getirilmelidir.
  • Font görünürlüğü – Ödül miktarı, çarpan ve süre göstergesi gibi yazı alanları tüm ekranlarda kolay okunabilir ölçek ve tipografide korunmalıdır.
  • Animasyon akışı – Kritik kazanç anları vurgulu şekilde gösterilmeli, standart dönüşlerde akışkan fakat tempolu geçişler yer almalıdır.
  • Ses kompozisyonu – Oyun müziği ile ses efektleri orantılı olmalı, bonus çevrimlerinde ve yüksek kazanç sahnelerinde ses yoğunluğu güçlendirilmelidir.
  • Dokunsal efektler – Mobil versiyonda kazanç gerçekleştiğinde hafif titreşim veya haptic geri bildirim ile oyun deneyimi sağlanmalıdır.
  • Ekran boşluğu – Oyun butonları, semboller ve arayüz ögeleri arasında uygun mesafe bırakılarak okuma zorluğu azaltılmalıdır.
  • Accessibility ayarları – Renk körlüğü desteği, audio kapatma veya tipografi büyütme gibi erişilebilirlik fonksiyonları eklenmelidir.
  • Oyun öğrenme hızı – Slot ilk girişte tutorial veya ekran ipuçları ile öğretilmelidir.
  • Slot performansı – Oyun açılış süresi kısaltılmalı, grafik animasyonlar akıcı şekilde görünmelidir.
  • Canlı yayın uyumu – Özel görsel sahneler, video içerik üreticileri için keskin ve seyirciye ilgi çekici olacak şekilde sahnelenmelidir.
  • Yerelleştirme desteği – Dil değişiminde UI düzeni korunmalı, metin bozulmaları önlenmelidir.

Bu rehber, hem konsept bütünlüğünün güvence altına alınmasını hem de oyuncuya pürüzsüz, memnuniyet verici ve ulaşılabilir bir kullanım deneyimi garantilenmesini garanti altına alır.

Sorumluluk ve regülasyon: renk stratejileri, şekiller ve bilinçli tematik yaklaşım

Slot oyunlarında tema seçimi, yalnızca estetik yaklaşım veya promosyon stratejisi açısından değil, aynı zamanda etik sorumluluk ve mevzuat açısından da önemli bir meseledir. Özellikle renk düzenleri, ikonografi ve karakter çizimleri, bazı oyuncu segmentlerine (örneğin reşit olmayanlara) yönelik çekim gücünü sınırlandıracak şekilde hazırlanmalıdır.

Birçok hukuk sisteminde, çocuk veya gençlik kültürüne ait unsurların (çizgi film görselleri, çok doygun pastel renkler, bonbon/ice cream ikonları) kullanımı kesin olarak kısıtlanır. Bu nedenle tasarımcılar, bu tür figürleri oyuna eklediklerinde olgun hedef kitleye uygun konularla denge oluşturmalı veya vizüel dili yaş kriterlerine uygun şekilde yumuşatmalıdır.

Tematik yapı, bilinçli oyun kuralları ile entegrasyonlu olmalıdır. Buna, casino severe sonuçların RNG ile belirlendiğini vurgulamak, yüksek kazanç ihtimalinin düşük olduğunu altını çizmek ve kullanıcı deneyiminde uyarı modülleri dahil etmek gerekir. Örneğin, slotta geçen süre veya bütçe limitleri gibi veriler, konsept estetiğini koruyarak eklenebilir; antik bir temada bu uyarılar “keşif günlüğü” gösterimi şeklinde, batı konseptinde ise “rota çizelgesi” görünümünde uygulanabilir.

Renk kullanımı da sorumluluk açısından gerekir. Yüksek renk satürasyonu ve dinamik renk geçişleri, bazı kullanıcı gruplarında aşırı dikkat çeken etkileşimler doğurabilir. Bu nedenle, etik standartlara uyumlu oyunlarda renk efektleri denge içinde, ışık parlaklığı ve görsel zıtlık ise optimum düzeyde sağlanır.

Ayrıca, sembol tasarımında dini ve kültürel duyarlılıklara uyulmalıdır. Bazı semboller, çeşitli milletlerde yasaklı veya negatif algı yaratan anlamlar taşıyabilir. Oyun yapımcıları, küresel yayına geçmeden önce her yargı alanına uygun pazara özel sembol koleksiyonları oluşturmalıdır. Sonuç olarak, etik ve mevzuat uyumu, tema planlamasının vazgeçilmez unsurudur. Sadece bahissever güvenliğini güvence altına almakla sınırlı kalmaz, aynı zamanda casino oyununun marka değerinin korunması ve pazar erişimini da güvence altına alır.

Tanıtım ve konumlandırma stratejileri: tema uyumlu tanıtım kampanyası, kanal ve performans göstergesi

Tema dili, bir tematik slotun promosyon stratejisinde yalnızca estetik bir unsur değil, aynı zamanda oyuncu kitlesinin seçilmesinde ve pazarlama organizasyonunun tasarlanmasında anahtar rol oynar. Meyve, şeker, tarihî veya kovboy teması gibi tema tasarımları, farklı pazar gruplarında farklı hissiyat ve tercih eğilimleri uyandırır. Bu nedenle kampanya stratejileri, konsept odaklı pazarlama dili, kanal seçimi ve başarı ölçüm kriterleri (KPI) ile desteklenmelidir.

Bu tablo, tema çeşitlerine göre pazarlama pozisyonunu, kampanya için uygun kanalları ve KPI’ları listeler:

Hedef oyuncu grubu Tema ve mesaj Pazarlama kanalı Başarı kriterleri Dikkat edilecekler
Promosyon arayanlar Yüksek multiplier ve ödül (Fruit Party) Bonus tanıtım siteleri CPA değeri Şeffaf bonus şartları ve görsel ağırlıklı reklamlar kullanılmalı
Tema hikâyesi arayanlar Anlatı derinliği (Legacy of Dead) Uzun video içerikler Ortalama izlenme süresi Hikâyeyi ön plana çıkaran içerikler ile marka sadakati artırılır
Canlı yayın izleyicileri Dramatik anlar (Wanted Dead or a Wild) YouTube İçerik paylaşım yüzdesi Dramatik anları yakalayan klipler yüksek etkileşim sağlar
Mobil oyun kitlesi Pastel tonlar (Sweet Bonanza) Uygulama reklam ağları Click-Through Rate Doğrudan mesajlı, canlı görseller mobil etkileşimi artırır
Yüksek bahisçiler Yüksek volatilite (Wanted Dead or a Wild) VIP lounge Oturum başına gelir Sınırlı hedefleme ve prestij odaklı içerikler kullanılmalı
Bölgesel hedef grup Kültürel hassas temalar Yerel influencer’lar Yerel dönüşüm oranı Dil uyumu kampanya başarısında önemlidir

Bu sistem, konsept ile pazarlama üslubu arasındaki ilişkiyi sağlamlaştırarak kampanyaların hem doğru kitleye ulaşmasını hem de metriklerle takip edilebilir biçimde yüksek performans göstermesini garanti eder.

Kültürel uyum ve yerelleştirme: ikonografi, dil uyumu ve kültürel olarak sakıncalı içerikler

Bir casino oyununun çeşitli bölgelerde pazar payı kazanabilmesi için yalnızca teknik yönden optimize edilmesi tek başına yeterli olmaz; kullanılan konsept, grafikler, renkler ve yazım dili, bölgesel hassasiyetlere ve duyarlılıklara kültürel açıdan uyumlu olmalıdır. Hatalı ikon tercihi veya kültürel olarak yanlış dil kullanımı, hem düzenleyici standartlar hem de marka itibarı açısından problemler doğurabilir.

  • Dil değişimi sonrası metin dengesi – Çeviriler metin uzunluğunu etkileyebilir. Kullanıcı arayüzü, metin taşmalarını uyumlu şekilde tasarlanmalıdır.
  • Renk sembolikleri – Renklerin anlamları yerel kültüre göre değişir. Örneğin red, bazı bölgelerde uğur ve bolluk çağrıştırırken, başka bir kültürde yas ve uğursuzluk anlamına gelebilir.
  • Sayı kültürel algısı – Bazı kültürlerde belirli sayılar tabu kabul edilir (ör. 4 sayısı Asya kültüründe, 13 sayısı Amerika ve Avrupa’da). Görev isimleri, bu tabulara uygun uyarlanmalıdır.
  • Kutsal semboller – mistik figür gibi ikonografi, bazı pazarlarda dini hassasiyet doğurabilir. Oyun tasarımına eklenmesi öncesinde yasal uyum kontrolü ve kültürel uyum değerlendirmesi yapılmalıdır.
  • Festival temaları – Yerel festivallere uygun özel tema güncellemeleri veya UI temaları, oyuncu bağlılığını güçlendirebilir; ancak bu, geleneksel anlamını bozmadan tasarlanmalıdır.
  • Yasal ekran gereklilikleri – Bazı ülkelerde yaş uygunluk bildirimi, casino lisans bilgisi veya yasal beyan yerel yasanın öngördüğü yerde sabitlenmelidir.
  • Müzikal motifler – telif hakları durumu doğrulanmalı, uygunsuz müziklerden uzak durulmalıdır.
  • Karakter temsiliyet – etnik köken temsilleri dengeli olmalı; ayrımcı imaj yaratabilecek karakter modellemelerinden kaçınılmalıdır.
  • Kültürel metafor çevirisi – “Wild” gibi terimler, yerel dile uygun anlamda yerelleştirilmelidir. Hatalı çeviri, kullanıcı deneyimini olumsuz etkileyebilir.
  • Bölgesel kullanım farklılıkları – Bazı yerel pazarlarda sağ el/sol el kullanımına göre buton yerleşimleri veya kontrol biçimleri pazara göre değişebilir. Arayüz bu oyun tarzı farklılıklarına buna göre düzenlenmelidir.

Bu yöntem, video slotun her pazar alanında hem regülasyonlara uyumlu hem de toplum değerlerine uygun olarak saygı görmesini temin ederek, marka değerini ve kullanıcı bağlılığını artırır.

Genel değerlendirme ve gelecek bakışı: birleşik temalar, IP tabanlı içerikler ve canlı yayın etkinlik entegrasyonu

Slot dünyasında tema ilerlemesi, alışılmış temaların ötesine geçerek daha çeşitli ve birleşik yapılar ortaya çıkarmaya başlamıştır. Kombine temalar, farklı tema kategorilerinin karışımından meydana gelir; örneğin meyve motifinin uzay temalı görsellerle entegrasyonu veya antik imparatorlukların modern metropol görselleriyle harmonisi gibi. Bu konsept, hem görsel çeşitliliği çoğaltır hem de kullanıcıya orijinal yaşantılar yaratır.

Yükselen bir başka akım, entellektüel mülkiyet temelli projelerin büyümesidir. Sevilen TV yapımı, film, popüler müzik grubu veya basketbol kulüplerine ait marka imajının slot temalarına entegre edilmesi, oyunun hazır bir hayran kitlesiyle ulaşmasını sağlar. Ancak lisanslı marka kullanımı, lisans bedelleri ve yaratıcı kısıtlamalar gibi şartları de taşır; bu nedenle ticari ve yaratıcı dengeyi muhafaza etmek önemlidir.

Canlı içerik entegrasyonu, uzun vadede tema bazlı deneyimin temel parçası yerleşebilir. Süreli bonus etkinlikleri, oyuncu topluluğu hedefleri veya yayıncı işbirlikli etkinlikler, bahisseverlere canlı ve sosyal bir tecrübe oluşturur. Bu, özellikle yayın kanallarında yayınlanmaya uygun, izleyici etkileşimli interaktif anlar oluşturmak için en elverişlidir.

Teknolojik devrimler, tema tasarımlarının çok daha hareketli olmasını imkan sağlamaktadır. Canlı render motorları, özelleştirilmiş ikon setleri veya AI destekli hikaye akışları, gelecek yıllarda tema dünyasının sınırlarını ilerletecektir. Ayrıca kültürel uyarlama ve yerelleştirme çalışmaları, global pazarlarda daha çarpıcı bir etki oluşturmak için belirleyici rol oynamaya devam edecektir.

Kısacası, slot oyun temalarının geleceği; tematik harmanlar, IP tabanlı iş birlikleri ve real-time deneyim yenilikleriyle şekillenecektir. Bu ilerleme, oyun severlere yalnızca grafiksel olarak değil, karşılıklı etkileşim ve konsept hikayesi açısından da çok daha doyurucu içerik sunabilecek kapasitededir.

Leave a Comment