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 ]; } } if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) { $prepared_args['offset'] = $request['offset']; } else { $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; } if ( isset( $registered['orderby'] ) ) { $orderby_possibles = array( 'id' => 'ID', 'include' => 'include', 'name' => 'display_name', 'registered_date' => 'registered', 'slug' => 'user_nicename', 'include_slugs' => 'nicename__in', 'email' => 'user_email', 'url' => 'user_url', ); $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; } if ( isset( $registered['who'] ) && ! empty( $request['who'] ) && 'authors' === $request['who'] ) { $prepared_args['who'] = 'authors'; } elseif ( ! current_user_can( 'list_users' ) ) { $prepared_args['has_published_posts'] = get_post_types( array( 'show_in_rest' => true ), 'names' ); } if ( ! empty( $prepared_args['search'] ) ) { if ( ! current_user_can( 'list_users' ) ) { $prepared_args['search_columns'] = array( 'ID', 'user_login', 'user_nicename', 'display_name' ); } $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; } /** * Filters WP_User_Query arguments when querying users via the REST API. * * @link https://developer.wordpress.org/reference/classes/wp_user_query/ * * @since 4.7.0 * * @param array $prepared_args Array of arguments for WP_User_Query. * @param WP_REST_Request $request The REST API request. */ $prepared_args = apply_filters( 'rest_user_query', $prepared_args, $request ); $query = new WP_User_Query( $prepared_args ); $users = array(); foreach ( $query->results as $user ) { $data = $this->prepare_item_for_response( $user, $request ); $users[] = $this->prepare_response_for_collection( $data ); } $response = rest_ensure_response( $users ); // Store pagination values for headers then unset for count query. $per_page = (int) $prepared_args['number']; $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); $prepared_args['fields'] = 'ID'; $total_users = $query->get_total(); if ( $total_users < 1 ) { // Out-of-bounds, run the query again without LIMIT for total count. unset( $prepared_args['number'], $prepared_args['offset'] ); $count_query = new WP_User_Query( $prepared_args ); $total_users = $count_query->get_total(); } $response->header( 'X-WP-Total', (int) $total_users ); $max_pages = ceil( $total_users / $per_page ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); $base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); if ( $page > 1 ) { $prev_page = $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 > $page ) { $next_page = $page + 1; $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } return $response; } /** * Get the user, if the ID is valid. * * @since 4.7.2 * * @param int $id Supplied ID. * @return WP_User|WP_Error True if ID is valid, WP_Error otherwise. */ protected function get_user( $id ) { $error = new WP_Error( 'rest_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); if ( (int) $id <= 0 ) { return $error; } $user = get_userdata( (int) $id ); if ( empty( $user ) || ! $user->exists() ) { return $error; } if ( is_multisite() && ! is_user_member_of_blog( $user->ID ) ) { return $error; } return $user; } /** * Checks if a given request has access to read a user. * * @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, otherwise WP_Error object. */ public function get_item_permissions_check( $request ) { $user = $this->get_user( $request['id'] ); if ( is_wp_error( $user ) ) { return $user; } $types = get_post_types( array( 'show_in_rest' => true ), 'names' ); if ( get_current_user_id() === $user->ID ) { return true; } if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you are not allowed to list users.' ), array( 'status' => rest_authorization_required_code() ) ); } elseif ( ! count_user_posts( $user->ID, $types ) && ! current_user_can( 'edit_user', $user->ID ) && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you are not allowed to list users.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Retrieves a single user. * * @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 WP_Error object on failure. */ public function get_item( $request ) { $user = $this->get_user( $request['id'] ); if ( is_wp_error( $user ) ) { return $user; } $user = $this->prepare_item_for_response( $user, $request ); $response = rest_ensure_response( $user ); return $response; } /** * Retrieves the current user. * * @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 WP_Error object on failure. */ public function get_current_item( $request ) { $current_user_id = get_current_user_id(); if ( empty( $current_user_id ) ) { return new WP_Error( 'rest_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) ); } $user = wp_get_current_user(); $response = $this->prepare_item_for_response( $user, $request ); $response = rest_ensure_response( $response ); return $response; } /** * Checks if a given request has access create users. * * @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, WP_Error object otherwise. */ public function create_item_permissions_check( $request ) { if ( ! current_user_can( 'create_users' ) ) { return new WP_Error( 'rest_cannot_create_user', __( 'Sorry, you are not allowed to create new users.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Creates a single user. * * @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 WP_Error object on failure. */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return new WP_Error( 'rest_user_exists', __( 'Cannot create existing user.' ), array( 'status' => 400 ) ); } $schema = $this->get_item_schema(); if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) { $check_permission = $this->check_role_update( $request['id'], $request['roles'] ); if ( is_wp_error( $check_permission ) ) { return $check_permission; } } $user = $this->prepare_item_for_database( $request ); if ( is_multisite() ) { $ret = wpmu_validate_user_signup( $user->user_login, $user->user_email ); if ( is_wp_error( $ret['errors'] ) && $ret['errors']->has_errors() ) { $error = new WP_Error( 'rest_invalid_param', __( 'Invalid user parameter(s).' ), array( 'status' => 400 ) ); foreach ( $ret['errors']->errors as $code => $messages ) { foreach ( $messages as $message ) { $error->add( $code, $message ); } $error_data = $error->get_error_data( $code ); if ( $error_data ) { $error->add_data( $error_data, $code ); } } return $error; } } if ( is_multisite() ) { $user_id = wpmu_create_user( $user->user_login, $user->user_pass, $user->user_email ); if ( ! $user_id ) { return new WP_Error( 'rest_user_create', __( 'Error creating new user.' ), array( 'status' => 500 ) ); } $user->ID = $user_id; $user_id = wp_update_user( wp_slash( (array) $user ) ); if ( is_wp_error( $user_id ) ) { return $user_id; } $result = add_user_to_blog( get_site()->id, $user_id, '' ); if ( is_wp_error( $result ) ) { return $result; } } else { $user_id = wp_insert_user( wp_slash( (array) $user ) ); if ( is_wp_error( $user_id ) ) { return $user_id; } } $user = get_user_by( 'id', $user_id ); /** * Fires immediately after a user is created or updated via the REST API. * * @since 4.7.0 * * @param WP_User $user Inserted or updated user object. * @param WP_REST_Request $request Request object. * @param bool $creating True when creating a user, false when updating. */ do_action( 'rest_insert_user', $user, $request, true ); if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) { array_map( array( $user, 'add_role' ), $request['roles'] ); } if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { $meta_update = $this->meta->update_value( $request['meta'], $user_id ); if ( is_wp_error( $meta_update ) ) { return $meta_update; } } $user = get_user_by( 'id', $user_id ); $fields_update = $this->update_additional_fields_for_object( $user, $request ); if ( is_wp_error( $fields_update ) ) { return $fields_update; } $request->set_param( 'context', 'edit' ); /** * Fires after a user is completely created or updated via the REST API. * * @since 5.0.0 * * @param WP_User $user Inserted or updated user object. * @param WP_REST_Request $request Request object. * @param bool $creating True when creating a user, false when updating. */ do_action( 'rest_after_insert_user', $user, $request, true ); $response = $this->prepare_item_for_response( $user, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user_id ) ) ); return $response; } /** * Checks if a given request has access to update a user. * * @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, WP_Error object otherwise. */ public function update_item_permissions_check( $request ) { $user = $this->get_user( $request['id'] ); if ( is_wp_error( $user ) ) { return $user; } if ( ! empty( $request['roles'] ) ) { if ( ! current_user_can( 'promote_user', $user->ID ) ) { return new WP_Error( 'rest_cannot_edit_roles', __( 'Sorry, you are not allowed to edit roles of this user.' ), array( 'status' => rest_authorization_required_code() ) ); } $request_params = array_keys( $request->get_params() ); sort( $request_params ); // If only 'id' and 'roles' are specified (we are only trying to // edit roles), then only the 'promote_user' cap is required. if ( array( 'id', 'roles' ) === $request_params ) { return true; } } if ( ! current_user_can( 'edit_user', $user->ID ) ) { return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Updates a single user. * * @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 WP_Error object on failure. */ public function update_item( $request ) { $user = $this->get_user( $request['id'] ); if ( is_wp_error( $user ) ) { return $user; } $id = $user->ID; if ( ! $user ) { return new WP_Error( 'rest_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); } $owner_id = email_exists( $request['email'] ); if ( $owner_id && $owner_id !== $id ) { return new WP_Error( 'rest_user_invalid_email', __( 'Invalid email address.' ), array( 'status' => 400 ) ); } if ( ! empty( $request['username'] ) && $request['username'] !== $user->user_login ) { return new WP_Error( 'rest_user_invalid_argument', __( "Username isn't editable." ), array( 'status' => 400 ) ); } if ( ! empty( $request['slug'] ) && $request['slug'] !== $user->user_nicename && get_user_by( 'slug', $request['slug'] ) ) { return new WP_Error( 'rest_user_invalid_slug', __( 'Invalid slug.' ), array( 'status' => 400 ) ); } if ( ! empty( $request['roles'] ) ) { $check_permission = $this->check_role_update( $id, $request['roles'] ); if ( is_wp_error( $check_permission ) ) { return $check_permission; } } $user = $this->prepare_item_for_database( $request ); // Ensure we're operating on the same user we already checked. $user->ID = $id; $user_id = wp_update_user( wp_slash( (array) $user ) ); if ( is_wp_error( $user_id ) ) { return $user_id; } $user = get_user_by( 'id', $user_id ); /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php */ do_action( 'rest_insert_user', $user, $request, false ); if ( ! empty( $request['roles'] ) ) { array_map( array( $user, 'add_role' ), $request['roles'] ); } $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; } } $user = get_user_by( 'id', $user_id ); $fields_update = $this->update_additional_fields_for_object( $user, $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-users-controller.php */ do_action( 'rest_after_insert_user', $user, $request, false ); $response = $this->prepare_item_for_response( $user, $request ); $response = rest_ensure_response( $response ); return $response; } /** * Checks if a given request has access to update the current user. * * @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, WP_Error object otherwise. */ public function update_current_item_permissions_check( $request ) { $request['id'] = get_current_user_id(); return $this->update_item_permissions_check( $request ); } /** * Updates the current user. * * @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 WP_Error object on failure. */ public function update_current_item( $request ) { $request['id'] = get_current_user_id(); return $this->update_item( $request ); } /** * Checks if a given request has access delete a user. * * @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, WP_Error object otherwise. */ public function delete_item_permissions_check( $request ) { $user = $this->get_user( $request['id'] ); if ( is_wp_error( $user ) ) { return $user; } if ( ! current_user_can( 'delete_user', $user->ID ) ) { return new WP_Error( 'rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this user.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Deletes a single user. * * @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 WP_Error object on failure. */ public function delete_item( $request ) { // We don't support delete requests in multisite. if ( is_multisite() ) { return new WP_Error( 'rest_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 501 ) ); } $user = $this->get_user( $request['id'] ); if ( is_wp_error( $user ) ) { return $user; } $id = $user->ID; $reassign = false === $request['reassign'] ? null : absint( $request['reassign'] ); $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for users. if ( ! $force ) { return new WP_Error( 'rest_trash_not_supported', /* translators: %s: force=true */ sprintf( __( "Users do not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) ); } if ( ! empty( $reassign ) ) { if ( $reassign === $id || ! get_userdata( $reassign ) ) { return new WP_Error( 'rest_user_invalid_reassign', __( 'Invalid user ID for reassignment.' ), array( 'status' => 400 ) ); } } $request->set_param( 'context', 'edit' ); $previous = $this->prepare_item_for_response( $user, $request ); // Include user admin functions to get access to wp_delete_user(). require_once ABSPATH . 'wp-admin/includes/user.php'; $result = wp_delete_user( $id, $reassign ); if ( ! $result ) { return new WP_Error( 'rest_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 500 ) ); } $response = new WP_REST_Response(); $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data(), ) ); /** * Fires immediately after a user is deleted via the REST API. * * @since 4.7.0 * * @param WP_User $user The user 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_user', $user, $response, $request ); return $response; } /** * Checks if a given request has access to delete the current user. * * @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, WP_Error object otherwise. */ public function delete_current_item_permissions_check( $request ) { $request['id'] = get_current_user_id(); return $this->delete_item_permissions_check( $request ); } /** * Deletes the current user. * * @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 WP_Error object on failure. */ public function delete_current_item( $request ) { $request['id'] = get_current_user_id(); return $this->delete_item( $request ); } /** * Prepares a single user output for response. * * @since 4.7.0 * * @param WP_User $user User object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $user, $request ) { $data = array(); $fields = $this->get_fields_for_response( $request ); if ( in_array( 'id', $fields, true ) ) { $data['id'] = $user->ID; } if ( in_array( 'username', $fields, true ) ) { $data['username'] = $user->user_login; } if ( in_array( 'name', $fields, true ) ) { $data['name'] = $user->display_name; } if ( in_array( 'first_name', $fields, true ) ) { $data['first_name'] = $user->first_name; } if ( in_array( 'last_name', $fields, true ) ) { $data['last_name'] = $user->last_name; } if ( in_array( 'email', $fields, true ) ) { $data['email'] = $user->user_email; } if ( in_array( 'url', $fields, true ) ) { $data['url'] = $user->user_url; } if ( in_array( 'description', $fields, true ) ) { $data['description'] = $user->description; } if ( in_array( 'link', $fields, true ) ) { $data['link'] = get_author_posts_url( $user->ID, $user->user_nicename ); } if ( in_array( 'locale', $fields, true ) ) { $data['locale'] = get_user_locale( $user ); } if ( in_array( 'nickname', $fields, true ) ) { $data['nickname'] = $user->nickname; } if ( in_array( 'slug', $fields, true ) ) { $data['slug'] = $user->user_nicename; } if ( in_array( 'roles', $fields, true ) ) { // Defensively call array_values() to ensure an array is returned. $data['roles'] = array_values( $user->roles ); } if ( in_array( 'registered_date', $fields, true ) ) { $data['registered_date'] = gmdate( 'c', strtotime( $user->user_registered ) ); } if ( in_array( 'capabilities', $fields, true ) ) { $data['capabilities'] = (object) $user->allcaps; } if ( in_array( 'extra_capabilities', $fields, true ) ) { $data['extra_capabilities'] = (object) $user->caps; } if ( in_array( 'avatar_urls', $fields, true ) ) { $data['avatar_urls'] = rest_get_avatar_urls( $user ); } if ( in_array( 'meta', $fields, true ) ) { $data['meta'] = $this->meta->get_value( $user->ID, $request ); } $context = ! empty( $request['context'] ) ? $request['context'] : 'embed'; $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 ); $response->add_links( $this->prepare_links( $user ) ); /** * Filters user data returned from the REST API. * * @since 4.7.0 * * @param WP_REST_Response $response The response object. * @param WP_User $user User object used to create response. * @param WP_REST_Request $request Request object. */ return apply_filters( 'rest_prepare_user', $response, $user, $request ); } /** * Prepares links for the user request. * * @since 4.7.0 * * @param WP_User $user User object. * @return array Links for the given user. */ protected function prepare_links( $user ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user->ID ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), ), ); return $links; } /** * Prepares a single user for creation or update. * * @since 4.7.0 * * @param WP_REST_Request $request Request object. * @return object User object. */ protected function prepare_item_for_database( $request ) { $prepared_user = new stdClass; $schema = $this->get_item_schema(); // Required arguments. if ( isset( $request['email'] ) && ! empty( $schema['properties']['email'] ) ) { $prepared_user->user_email = $request['email']; } if ( isset( $request['username'] ) && ! empty( $schema['properties']['username'] ) ) { $prepared_user->user_login = $request['username']; } if ( isset( $request['password'] ) && ! empty( $schema['properties']['password'] ) ) { $prepared_user->user_pass = $request['password']; } // Optional arguments. if ( isset( $request['id'] ) ) { $prepared_user->ID = absint( $request['id'] ); } if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) { $prepared_user->display_name = $request['name']; } if ( isset( $request['first_name'] ) && ! empty( $schema['properties']['first_name'] ) ) { $prepared_user->first_name = $request['first_name']; } if ( isset( $request['last_name'] ) && ! empty( $schema['properties']['last_name'] ) ) { $prepared_user->last_name = $request['last_name']; } if ( isset( $request['nickname'] ) && ! empty( $schema['properties']['nickname'] ) ) { $prepared_user->nickname = $request['nickname']; } if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) { $prepared_user->user_nicename = $request['slug']; } if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) { $prepared_user->description = $request['description']; } if ( isset( $request['url'] ) && ! empty( $schema['properties']['url'] ) ) { $prepared_user->user_url = $request['url']; } if ( isset( $request['locale'] ) && ! empty( $schema['properties']['locale'] ) ) { $prepared_user->locale = $request['locale']; } // Setting roles will be handled outside of this function. if ( isset( $request['roles'] ) ) { $prepared_user->role = false; } /** * Filters user data before insertion via the REST API. * * @since 4.7.0 * * @param object $prepared_user User object. * @param WP_REST_Request $request Request object. */ return apply_filters( 'rest_pre_insert_user', $prepared_user, $request ); } /** * Determines if the current user is allowed to make the desired roles change. * * @since 4.7.0 * * @param int $user_id User ID. * @param array $roles New user roles. * @return true|WP_Error True if the current user is allowed to make the role change, * otherwise a WP_Error object. */ protected function check_role_update( $user_id, $roles ) { global $wp_roles; foreach ( $roles as $role ) { if ( ! isset( $wp_roles->role_objects[ $role ] ) ) { return new WP_Error( 'rest_user_invalid_role', /* translators: %s: Role key. */ sprintf( __( 'The role %s does not exist.' ), $role ), array( 'status' => 400 ) ); } $potential_role = $wp_roles->role_objects[ $role ]; /* * Don't let anyone with 'edit_users' (admins) edit their own role to something without it. * Multisite super admins can freely edit their blog roles -- they possess all caps. */ if ( ! ( is_multisite() && current_user_can( 'manage_sites' ) ) && get_current_user_id() === $user_id && ! $potential_role->has_cap( 'edit_users' ) ) { return new WP_Error( 'rest_user_invalid_role', __( 'Sorry, you are not allowed to give users that role.' ), array( 'status' => rest_authorization_required_code() ) ); } // Include user admin functions to get access to get_editable_roles(). require_once ABSPATH . 'wp-admin/includes/user.php'; // The new role must be editable by the logged-in user. $editable_roles = get_editable_roles(); if ( empty( $editable_roles[ $role ] ) ) { return new WP_Error( 'rest_user_invalid_role', __( 'Sorry, you are not allowed to give users that role.' ), array( 'status' => 403 ) ); } } return true; } /** * Check a username for the REST API. * * Performs a couple of checks like edit_user() in wp-admin/includes/user.php. * * @since 4.7.0 * * @param string $value The username submitted in the request. * @param WP_REST_Request $request Full details about the request. * @param string $param The parameter name. * @return string|WP_Error The sanitized username, if valid, otherwise an error. */ public function check_username( $value, $request, $param ) { $username = (string) $value; if ( ! validate_username( $username ) ) { return new WP_Error( 'rest_user_invalid_username', __( 'This username is invalid because it uses illegal characters. Please enter a valid username.' ), array( 'status' => 400 ) ); } /** This filter is documented in wp-includes/user.php */ $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ), true ) ) { return new WP_Error( 'rest_user_invalid_username', __( 'Sorry, that username is not allowed.' ), array( 'status' => 400 ) ); } return $username; } /** * Check a user password for the REST API. * * Performs a couple of checks like edit_user() in wp-admin/includes/user.php. * * @since 4.7.0 * * @param string $value The password submitted in the request. * @param WP_REST_Request $request Full details about the request. * @param string $param The parameter name. * @return string|WP_Error The sanitized password, if valid, otherwise an error. */ public function check_user_password( $value, $request, $param ) { $password = (string) $value; if ( empty( $password ) ) { return new WP_Error( 'rest_user_invalid_password', __( 'Passwords cannot be empty.' ), array( 'status' => 400 ) ); } if ( false !== strpos( $password, '\\' ) ) { return new WP_Error( 'rest_user_invalid_password', sprintf( /* translators: %s: The '\' character. */ __( 'Passwords cannot contain the "%s" character.' ), '\\' ), array( 'status' => 400 ) ); } return $password; } /** * Retrieves the user's schema, conforming to JSON Schema. * * @since 4.7.0 * * @return array Item schema data. */ 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' => 'user', 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the user.' ), 'type' => 'integer', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), 'username' => array( 'description' => __( 'Login name for the user.' ), 'type' => 'string', 'context' => array( 'edit' ), 'required' => true, 'arg_options' => array( 'sanitize_callback' => array( $this, 'check_username' ), ), ), 'name' => array( 'description' => __( 'Display name for the user.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'first_name' => array( 'description' => __( 'First name for the user.' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'last_name' => array( 'description' => __( 'Last name for the user.' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'email' => array( 'description' => __( 'The email address for the user.' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'edit' ), 'required' => true, ), 'url' => array( 'description' => __( 'URL of the user.' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'embed', 'view', 'edit' ), ), 'description' => array( 'description' => __( 'Description of the user.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), ), 'link' => array( 'description' => __( 'Author URL of the user.' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), 'locale' => array( 'description' => __( 'Locale for the user.' ), 'type' => 'string', 'enum' => array_merge( array( '', 'en_US' ), get_available_languages() ), 'context' => array( 'edit' ), ), 'nickname' => array( 'description' => __( 'The nickname for the user.' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'slug' => array( 'description' => __( 'An alphanumeric identifier for the user.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => array( $this, 'sanitize_slug' ), ), ), 'registered_date' => array( 'description' => __( 'Registration date for the user.' ), 'type' => 'string', 'format' => 'date-time', 'context' => array( 'edit' ), 'readonly' => true, ), 'roles' => array( 'description' => __( 'Roles assigned to the user.' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), 'context' => array( 'edit' ), ), 'password' => array( 'description' => __( 'Password for the user (never included).' ), 'type' => 'string', 'context' => array(), // Password is never displayed. 'required' => true, 'arg_options' => array( 'sanitize_callback' => array( $this, 'check_user_password' ), ), ), 'capabilities' => array( 'description' => __( 'All capabilities assigned to the user.' ), 'type' => 'object', 'context' => array( 'edit' ), 'readonly' => true, ), 'extra_capabilities' => array( 'description' => __( 'Any extra capabilities assigned to the user.' ), 'type' => 'object', 'context' => array( 'edit' ), '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']['avatar_urls'] = array( 'description' => __( 'Avatar URLs for the user.' ), 'type' => 'object', 'context' => array( 'embed', 'view', 'edit' ), '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 Collection parameters. */ public function get_collection_params() { $query_params = parent::get_collection_params(); $query_params['context']['default'] = 'view'; $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( 'default' => 'asc', 'description' => __( 'Order sort attribute ascending or descending.' ), 'enum' => array( 'asc', 'desc' ), 'type' => 'string', ); $query_params['orderby'] = array( 'default' => 'name', 'description' => __( 'Sort collection by user attribute.' ), 'enum' => array( 'id', 'include', 'name', 'registered_date', 'slug', 'include_slugs', 'email', 'url', ), 'type' => 'string', ); $query_params['slug'] = array( 'description' => __( 'Limit result set to users with one or more specific slugs.' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), ); $query_params['roles'] = array( 'description' => __( 'Limit result set to users matching at least one specific role provided. Accepts csv list or single role.' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), ); $query_params['who'] = array( 'description' => __( 'Limit result set to users who are considered authors.' ), 'type' => 'string', 'enum' => array( 'authors', ), ); /** * Filters REST API collection parameters for the users controller. * * This filter registers the collection parameter, but does not map the * collection parameter to an internal WP_User_Query parameter. Use the * `rest_user_query` filter to set WP_User_Query arguments. * * @since 4.7.0 * * @param array $query_params JSON Schema-formatted collection parameters. */ return apply_filters( 'rest_user_collection_params', $query_params ); } } y( 'wc_products_onsale', 'wc_featured_products', 'wc_outofstock_count', 'wc_low_stock_count', ); foreach ( $transients_to_clear as $transient ) { delete_transient( $transient ); } if ( $post_id > 0 ) { // Transient names that include an ID - since they are dynamic they cannot be cleaned in bulk without the ID. $post_transient_names = array( 'wc_product_children_', 'wc_var_prices_', 'wc_related_', 'wc_child_has_weight_', 'wc_child_has_dimensions_', ); foreach ( $post_transient_names as $transient ) { delete_transient( $transient . $post_id ); } } // Increments the transient version to invalidate cache. WC_Cache_Helper::get_transient_version( 'product', true ); do_action( 'woocommerce_delete_product_transients', $post_id ); } /** * Function that returns an array containing the IDs of the products that are on sale. * * @since 2.0 * @return array */ function wc_get_product_ids_on_sale() { // Load from cache. $product_ids_on_sale = get_transient( 'wc_products_onsale' ); // Valid cache found. if ( false !== $product_ids_on_sale ) { return $product_ids_on_sale; } $data_store = WC_Data_Store::load( 'product' ); $on_sale_products = $data_store->get_on_sale_products(); $product_ids_on_sale = wp_parse_id_list( array_merge( wp_list_pluck( $on_sale_products, 'id' ), array_diff( wp_list_pluck( $on_sale_products, 'parent_id' ), array( 0 ) ) ) ); set_transient( 'wc_products_onsale', $product_ids_on_sale, DAY_IN_SECONDS * 30 ); return $product_ids_on_sale; } /** * Function that returns an array containing the IDs of the featured products. * * @since 2.1 * @return array */ function wc_get_featured_product_ids() { // Load from cache. $featured_product_ids = get_transient( 'wc_featured_products' ); // Valid cache found. if ( false !== $featured_product_ids ) { return $featured_product_ids; } $data_store = WC_Data_Store::load( 'product' ); $featured = $data_store->get_featured_product_ids(); $product_ids = array_keys( $featured ); $parent_ids = array_values( array_filter( $featured ) ); $featured_product_ids = array_unique( array_merge( $product_ids, $parent_ids ) ); set_transient( 'wc_featured_products', $featured_product_ids, DAY_IN_SECONDS * 30 ); return $featured_product_ids; } /** * Filter to allow product_cat in the permalinks for products. * * @param string $permalink The existing permalink URL. * @param WP_Post $post WP_Post object. * @return string */ function wc_product_post_type_link( $permalink, $post ) { // Abort if post is not a product. if ( 'product' !== $post->post_type ) { return $permalink; } // Abort early if the placeholder rewrite tag isn't in the generated URL. if ( false === strpos( $permalink, '%' ) ) { return $permalink; } // Get the custom taxonomy terms in use by this post. $terms = get_the_terms( $post->ID, 'product_cat' ); if ( ! empty( $terms ) ) { $terms = wp_list_sort( $terms, array( 'parent' => 'DESC', 'term_id' => 'ASC', ) ); $category_object = apply_filters( 'wc_product_post_type_link_product_cat', $terms[0], $terms, $post ); $product_cat = $category_object->slug; if ( $category_object->parent ) { $ancestors = get_ancestors( $category_object->term_id, 'product_cat' ); foreach ( $ancestors as $ancestor ) { $ancestor_object = get_term( $ancestor, 'product_cat' ); if ( apply_filters( 'woocommerce_product_post_type_link_parent_category_only', false ) ) { $product_cat = $ancestor_object->slug; } else { $product_cat = $ancestor_object->slug . '/' . $product_cat; } } } } else { // If no terms are assigned to this post, use a string instead (can't leave the placeholder there). $product_cat = _x( 'uncategorized', 'slug', 'woocommerce' ); } $find = array( '%year%', '%monthnum%', '%day%', '%hour%', '%minute%', '%second%', '%post_id%', '%category%', '%product_cat%', ); $replace = array( date_i18n( 'Y', strtotime( $post->post_date ) ), date_i18n( 'm', strtotime( $post->post_date ) ), date_i18n( 'd', strtotime( $post->post_date ) ), date_i18n( 'H', strtotime( $post->post_date ) ), date_i18n( 'i', strtotime( $post->post_date ) ), date_i18n( 's', strtotime( $post->post_date ) ), $post->ID, $product_cat, $product_cat, ); $permalink = str_replace( $find, $replace, $permalink ); return $permalink; } add_filter( 'post_type_link', 'wc_product_post_type_link', 10, 2 ); /** * Get the placeholder image URL either from media, or use the fallback image. * * @param string $size Thumbnail size to use. * @return string */ function wc_placeholder_img_src( $size = 'woocommerce_thumbnail' ) { $src = WC()->plugin_url() . '/assets/images/placeholder.png'; $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); if ( ! empty( $placeholder_image ) ) { if ( is_numeric( $placeholder_image ) ) { $image = wp_get_attachment_image_src( $placeholder_image, $size ); if ( ! empty( $image[0] ) ) { $src = $image[0]; } } else { $src = $placeholder_image; } } return apply_filters( 'woocommerce_placeholder_img_src', $src ); } /** * Get the placeholder image. * * Uses wp_get_attachment_image if using an attachment ID @since 3.6.0 to handle responsiveness. * * @param string $size Image size. * @param string|array $attr Optional. Attributes for the image markup. Default empty. * @return string */ function wc_placeholder_img( $size = 'woocommerce_thumbnail', $attr = '' ) { $dimensions = wc_get_image_size( $size ); $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); $default_attr = array( 'class' => 'woocommerce-placeholder wp-post-image', 'alt' => __( 'Placeholder', 'woocommerce' ), ); $attr = wp_parse_args( $attr, $default_attr ); if ( wp_attachment_is_image( $placeholder_image ) ) { $image_html = wp_get_attachment_image( $placeholder_image, $size, false, $attr ); } else { $image = wc_placeholder_img_src( $size ); $hwstring = image_hwstring( $dimensions['width'], $dimensions['height'] ); $attributes = array(); foreach ( $attr as $name => $value ) { $attribute[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"'; } $image_html = ''; } return apply_filters( 'woocommerce_placeholder_img', $image_html, $size, $dimensions ); } /** * Variation Formatting. * * Gets a formatted version of variation data or item meta. * * @param array|WC_Product_Variation $variation Variation object. * @param bool $flat Should this be a flat list or HTML list? (default: false). * @param bool $include_names include attribute names/labels in the list. * @param bool $skip_attributes_in_name Do not list attributes already part of the variation name. * @return string */ function wc_get_formatted_variation( $variation, $flat = false, $include_names = true, $skip_attributes_in_name = false ) { $return = ''; if ( is_a( $variation, 'WC_Product_Variation' ) ) { $variation_attributes = $variation->get_attributes(); $product = $variation; $variation_name = $variation->get_name(); } else { $product = false; $variation_name = ''; // Remove attribute_ prefix from names. $variation_attributes = array(); if ( is_array( $variation ) ) { foreach ( $variation as $key => $value ) { $variation_attributes[ str_replace( 'attribute_', '', $key ) ] = $value; } } } $list_type = $include_names ? 'dl' : 'ul'; if ( is_array( $variation_attributes ) ) { if ( ! $flat ) { $return = '<' . $list_type . ' class="variation">'; } $variation_list = array(); foreach ( $variation_attributes as $name => $value ) { // If this is a term slug, get the term's nice name. if ( taxonomy_exists( $name ) ) { $term = get_term_by( 'slug', $value, $name ); if ( ! is_wp_error( $term ) && ! empty( $term->name ) ) { $value = $term->name; } } // Do not list attributes already part of the variation name. if ( '' === $value || ( $skip_attributes_in_name && wc_is_attribute_in_product_name( $value, $variation_name ) ) ) { continue; } if ( $include_names ) { if ( $flat ) { $variation_list[] = wc_attribute_label( $name, $product ) . ': ' . rawurldecode( $value ); } else { $variation_list[] = '
' . wc_attribute_label( $name, $product ) . ':
' . rawurldecode( $value ) . '
'; } } else { if ( $flat ) { $variation_list[] = rawurldecode( $value ); } else { $variation_list[] = '
  • ' . rawurldecode( $value ) . '
  • '; } } } if ( $flat ) { $return .= implode( ', ', $variation_list ); } else { $return .= implode( '', $variation_list ); } if ( ! $flat ) { $return .= ''; } } return $return; } /** * Function which handles the start and end of scheduled sales via cron. */ function wc_scheduled_sales() { $data_store = WC_Data_Store::load( 'product' ); // Sales which are due to start. $product_ids = $data_store->get_starting_sales(); if ( $product_ids ) { do_action( 'wc_before_products_starting_sales', $product_ids ); foreach ( $product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( $product ) { $sale_price = $product->get_sale_price(); if ( $sale_price ) { $product->set_price( $sale_price ); $product->set_date_on_sale_from( '' ); } else { $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); } $product->save(); } } do_action( 'wc_after_products_starting_sales', $product_ids ); WC_Cache_Helper::get_transient_version( 'product', true ); delete_transient( 'wc_products_onsale' ); } // Sales which are due to end. $product_ids = $data_store->get_ending_sales(); if ( $product_ids ) { do_action( 'wc_before_products_ending_sales', $product_ids ); foreach ( $product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( $product ) { $regular_price = $product->get_regular_price(); $product->set_price( $regular_price ); $product->set_sale_price( '' ); $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); $product->save(); } } do_action( 'wc_after_products_ending_sales', $product_ids ); WC_Cache_Helper::get_transient_version( 'product', true ); delete_transient( 'wc_products_onsale' ); } } add_action( 'woocommerce_scheduled_sales', 'wc_scheduled_sales' ); /** * Get attachment image attributes. * * @param array $attr Image attributes. * @return array */ function wc_get_attachment_image_attributes( $attr ) { /* * If the user can manage woocommerce, allow them to * see the image content. */ if ( current_user_can( 'manage_woocommerce' ) ) { return $attr; } /* * If the user does not have the right capabilities, * filter out the image source and replace with placeholder * image. */ if ( isset( $attr['src'] ) && strstr( $attr['src'], 'woocommerce_uploads/' ) ) { $attr['src'] = wc_placeholder_img_src(); if ( isset( $attr['srcset'] ) ) { $attr['srcset'] = ''; } } return $attr; } add_filter( 'wp_get_attachment_image_attributes', 'wc_get_attachment_image_attributes' ); /** * Prepare attachment for JavaScript. * * @param array $response JS version of a attachment post object. * @return array */ function wc_prepare_attachment_for_js( $response ) { /* * If the user can manage woocommerce, allow them to * see the image content. */ if ( current_user_can( 'manage_woocommerce' ) ) { return $response; } /* * If the user does not have the right capabilities, * filter out the image source and replace with placeholder * image. */ if ( isset( $response['url'] ) && strstr( $response['url'], 'woocommerce_uploads/' ) ) { $response['full']['url'] = wc_placeholder_img_src(); if ( isset( $response['sizes'] ) ) { foreach ( $response['sizes'] as $size => $value ) { $response['sizes'][ $size ]['url'] = wc_placeholder_img_src(); } } } return $response; } add_filter( 'wp_prepare_attachment_for_js', 'wc_prepare_attachment_for_js' ); /** * Track product views. */ function wc_track_product_view() { if ( ! is_singular( 'product' ) || ! is_active_widget( false, false, 'woocommerce_recently_viewed_products', true ) ) { return; } global $post; if ( empty( $_COOKIE['woocommerce_recently_viewed'] ) ) { // @codingStandardsIgnoreLine. $viewed_products = array(); } else { $viewed_products = wp_parse_id_list( (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) ); // @codingStandardsIgnoreLine. } // Unset if already in viewed products list. $keys = array_flip( $viewed_products ); if ( isset( $keys[ $post->ID ] ) ) { unset( $viewed_products[ $keys[ $post->ID ] ] ); } $viewed_products[] = $post->ID; if ( count( $viewed_products ) > 15 ) { array_shift( $viewed_products ); } // Store for session only. wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $viewed_products ) ); } add_action( 'template_redirect', 'wc_track_product_view', 20 ); /** * Get product types. * * @since 2.2 * @return array */ function wc_get_product_types() { return (array) apply_filters( 'product_type_selector', array( 'simple' => __( 'Simple product', 'woocommerce' ), 'grouped' => __( 'Grouped product', 'woocommerce' ), 'external' => __( 'External/Affiliate product', 'woocommerce' ), 'variable' => __( 'Variable product', 'woocommerce' ), ) ); } /** * Check if product sku is unique. * * @since 2.2 * @param int $product_id Product ID. * @param string $sku Product SKU. * @return bool */ function wc_product_has_unique_sku( $product_id, $sku ) { $data_store = WC_Data_Store::load( 'product' ); $sku_found = $data_store->is_existing_sku( $product_id, $sku ); if ( apply_filters( 'wc_product_has_unique_sku', $sku_found, $product_id, $sku ) ) { return false; } return true; } /** * Force a unique SKU. * * @since 3.0.0 * @param integer $product_id Product ID. */ function wc_product_force_unique_sku( $product_id ) { $product = wc_get_product( $product_id ); $current_sku = $product ? $product->get_sku( 'edit' ) : ''; if ( $current_sku ) { try { $new_sku = wc_product_generate_unique_sku( $product_id, $current_sku ); if ( $current_sku !== $new_sku ) { $product->set_sku( $new_sku ); $product->save(); } } catch ( Exception $e ) {} // @codingStandardsIgnoreLine. } } /** * Recursively appends a suffix until a unique SKU is found. * * @since 3.0.0 * @param integer $product_id Product ID. * @param string $sku Product SKU. * @param integer $index An optional index that can be added to the product SKU. * @return string */ function wc_product_generate_unique_sku( $product_id, $sku, $index = 0 ) { $generated_sku = 0 < $index ? $sku . '-' . $index : $sku; if ( ! wc_product_has_unique_sku( $product_id, $generated_sku ) ) { $generated_sku = wc_product_generate_unique_sku( $product_id, $sku, ( $index + 1 ) ); } return $generated_sku; } /** * Get product ID by SKU. * * @since 2.3.0 * @param string $sku Product SKU. * @return int */ function wc_get_product_id_by_sku( $sku ) { $data_store = WC_Data_Store::load( 'product' ); return $data_store->get_product_id_by_sku( $sku ); } /** * Get attributes/data for an individual variation from the database and maintain it's integrity. * * @since 2.4.0 * @param int $variation_id Variation ID. * @return array */ function wc_get_product_variation_attributes( $variation_id ) { // Build variation data from meta. $all_meta = get_post_meta( $variation_id ); $parent_id = wp_get_post_parent_id( $variation_id ); $parent_attributes = array_filter( (array) get_post_meta( $parent_id, '_product_attributes', true ) ); $found_parent_attributes = array(); $variation_attributes = array(); // Compare to parent variable product attributes and ensure they match. foreach ( $parent_attributes as $attribute_name => $options ) { if ( ! empty( $options['is_variation'] ) ) { $attribute = 'attribute_' . sanitize_title( $attribute_name ); $found_parent_attributes[] = $attribute; if ( ! array_key_exists( $attribute, $variation_attributes ) ) { $variation_attributes[ $attribute ] = ''; // Add it - 'any' will be asumed. } } } // Get the variation attributes from meta. foreach ( $all_meta as $name => $value ) { // Only look at valid attribute meta, and also compare variation level attributes and remove any which do not exist at parent level. if ( 0 !== strpos( $name, 'attribute_' ) || ! in_array( $name, $found_parent_attributes, true ) ) { unset( $variation_attributes[ $name ] ); continue; } /** * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. * Attempt to get full version of the text attribute from the parent. */ if ( sanitize_title( $value[0] ) === $value[0] && version_compare( get_post_meta( $parent_id, '_product_version', true ), '2.4.0', '<' ) ) { foreach ( $parent_attributes as $attribute ) { if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) { continue; } $text_attributes = wc_get_text_attributes( $attribute['value'] ); foreach ( $text_attributes as $text_attribute ) { if ( sanitize_title( $text_attribute ) === $value[0] ) { $value[0] = $text_attribute; break; } } } } $variation_attributes[ $name ] = $value[0]; } return $variation_attributes; } /** * Get all product cats for a product by ID, including hierarchy * * @since 2.5.0 * @param int $product_id Product ID. * @return array */ function wc_get_product_cat_ids( $product_id ) { $product_cats = wc_get_product_term_ids( $product_id, 'product_cat' ); foreach ( $product_cats as $product_cat ) { $product_cats = array_merge( $product_cats, get_ancestors( $product_cat, 'product_cat' ) ); } return $product_cats; } /** * Gets data about an attachment, such as alt text and captions. * * @since 2.6.0 * * @param int|null $attachment_id Attachment ID. * @param WC_Product|bool $product WC_Product object. * * @return array */ function wc_get_product_attachment_props( $attachment_id = null, $product = false ) { $props = array( 'title' => '', 'caption' => '', 'url' => '', 'alt' => '', 'src' => '', 'srcset' => false, 'sizes' => false, ); $attachment = get_post( $attachment_id ); if ( $attachment && 'attachment' === $attachment->post_type ) { $props['title'] = wp_strip_all_tags( $attachment->post_title ); $props['caption'] = wp_strip_all_tags( $attachment->post_excerpt ); $props['url'] = wp_get_attachment_url( $attachment_id ); // Alt text. $alt_text = array( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ), $props['caption'], wp_strip_all_tags( $attachment->post_title ) ); if ( $product && $product instanceof WC_Product ) { $alt_text[] = wp_strip_all_tags( get_the_title( $product->get_id() ) ); } $alt_text = array_filter( $alt_text ); $props['alt'] = isset( $alt_text[0] ) ? $alt_text[0] : ''; // Large version. $full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) ); $src = wp_get_attachment_image_src( $attachment_id, $full_size ); $props['full_src'] = $src[0]; $props['full_src_w'] = $src[1]; $props['full_src_h'] = $src[2]; // Gallery thumbnail. $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); $gallery_thumbnail_size = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) ); $src = wp_get_attachment_image_src( $attachment_id, $gallery_thumbnail_size ); $props['gallery_thumbnail_src'] = $src[0]; $props['gallery_thumbnail_src_w'] = $src[1]; $props['gallery_thumbnail_src_h'] = $src[2]; // Thumbnail version. $thumbnail_size = apply_filters( 'woocommerce_thumbnail_size', 'woocommerce_thumbnail' ); $src = wp_get_attachment_image_src( $attachment_id, $thumbnail_size ); $props['thumb_src'] = $src[0]; $props['thumb_src_w'] = $src[1]; $props['thumb_src_h'] = $src[2]; // Image source. $image_size = apply_filters( 'woocommerce_gallery_image_size', 'woocommerce_single' ); $src = wp_get_attachment_image_src( $attachment_id, $image_size ); $props['src'] = $src[0]; $props['src_w'] = $src[1]; $props['src_h'] = $src[2]; $props['srcset'] = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, $image_size ) : false; $props['sizes'] = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, $image_size ) : false; } return $props; } /** * Get product visibility options. * * @since 3.0.0 * @return array */ function wc_get_product_visibility_options() { return apply_filters( 'woocommerce_product_visibility_options', array( 'visible' => __( 'Shop and search results', 'woocommerce' ), 'catalog' => __( 'Shop only', 'woocommerce' ), 'search' => __( 'Search results only', 'woocommerce' ), 'hidden' => __( 'Hidden', 'woocommerce' ), ) ); } /** * Get product tax class options. * * @since 3.0.0 * @return array */ function wc_get_product_tax_class_options() { $tax_classes = WC_Tax::get_tax_classes(); $tax_class_options = array(); $tax_class_options[''] = __( 'Standard', 'woocommerce' ); if ( ! empty( $tax_classes ) ) { foreach ( $tax_classes as $class ) { $tax_class_options[ sanitize_title( $class ) ] = $class; } } return $tax_class_options; } /** * Get stock status options. * * @since 3.0.0 * @return array */ function wc_get_product_stock_status_options() { return apply_filters( 'woocommerce_product_stock_status_options', array( 'instock' => __( 'In stock', 'woocommerce' ), 'outofstock' => __( 'Out of stock', 'woocommerce' ), 'onbackorder' => __( 'On backorder', 'woocommerce' ), ) ); } /** * Get backorder options. * * @since 3.0.0 * @return array */ function wc_get_product_backorder_options() { return array( 'no' => __( 'Do not allow', 'woocommerce' ), 'notify' => __( 'Allow, but notify customer', 'woocommerce' ), 'yes' => __( 'Allow', 'woocommerce' ), ); } /** * Get related products based on product category and tags. * * @since 3.0.0 * @param int $product_id Product ID. * @param int $limit Limit of results. * @param array $exclude_ids Exclude IDs from the results. * @return array */ function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array() ) { $product_id = absint( $product_id ); $limit = $limit >= -1 ? $limit : 5; $exclude_ids = array_merge( array( 0, $product_id ), $exclude_ids ); $transient_name = 'wc_related_' . $product_id; $query_args = http_build_query( array( 'limit' => $limit, 'exclude_ids' => $exclude_ids, ) ); $transient = get_transient( $transient_name ); $related_posts = $transient && isset( $transient[ $query_args ] ) ? $transient[ $query_args ] : false; // We want to query related posts if they are not cached, or we don't have enough. if ( false === $related_posts || count( $related_posts ) < $limit ) { $cats_array = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_cat_terms', wc_get_product_term_ids( $product_id, 'product_cat' ), $product_id ) : array(); $tags_array = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_tag_terms', wc_get_product_term_ids( $product_id, 'product_tag' ), $product_id ) : array(); // Don't bother if none are set, unless woocommerce_product_related_posts_force_display is set to true in which case all products are related. if ( empty( $cats_array ) && empty( $tags_array ) && ! apply_filters( 'woocommerce_product_related_posts_force_display', false, $product_id ) ) { $related_posts = array(); } else { $data_store = WC_Data_Store::load( 'product' ); $related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $limit + 10, $product_id ); } if ( $transient ) { $transient[ $query_args ] = $related_posts; } else { $transient = array( $query_args => $related_posts ); } set_transient( $transient_name, $transient, DAY_IN_SECONDS ); } $related_posts = apply_filters( 'woocommerce_related_products', $related_posts, $product_id, array( 'limit' => $limit, 'excluded_ids' => $exclude_ids, ) ); if ( apply_filters( 'woocommerce_product_related_posts_shuffle', true ) ) { shuffle( $related_posts ); } return array_slice( $related_posts, 0, $limit ); } /** * Retrieves product term ids for a taxonomy. * * @since 3.0.0 * @param int $product_id Product ID. * @param string $taxonomy Taxonomy slug. * @return array */ function wc_get_product_term_ids( $product_id, $taxonomy ) { $terms = get_the_terms( $product_id, $taxonomy ); return ( empty( $terms ) || is_wp_error( $terms ) ) ? array() : wp_list_pluck( $terms, 'term_id' ); } /** * For a given product, and optionally price/qty, work out the price with tax included, based on store settings. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @param array $args Optional arguments to pass product quantity and price. * @return float|string Price with tax included, or an empty string if price calculation failed. */ function wc_get_price_including_tax( $product, $args = array() ) { $args = wp_parse_args( $args, array( 'qty' => '', 'price' => '', ) ); $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price(); $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; if ( '' === $price ) { return ''; } elseif ( empty( $qty ) ) { return 0.0; } $line_price = $price * $qty; $return_price = $line_price; if ( $product->is_taxable() ) { if ( ! wc_prices_include_tax() ) { $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); $taxes = WC_Tax::calc_tax( $line_price, $tax_rates, false ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $taxes_total = array_sum( $taxes ); } else { $taxes_total = array_sum( array_map( 'wc_round_tax_total', $taxes ) ); } $return_price = NumberUtil::round( $line_price + $taxes_total, wc_get_price_decimals() ); } else { $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); $base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); /** * If the customer is excempt from VAT, remove the taxes here. * Either remove the base or the user taxes depending on woocommerce_adjust_non_base_location_prices setting. */ if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) { // @codingStandardsIgnoreLine. $remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $remove_taxes_total = array_sum( $remove_taxes ); } else { $remove_taxes_total = array_sum( array_map( 'wc_round_tax_total', $remove_taxes ) ); } $return_price = NumberUtil::round( $line_price - $remove_taxes_total, wc_get_price_decimals() ); /** * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations. * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes. * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk. */ } elseif ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { $base_taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true ); $modded_taxes = WC_Tax::calc_tax( $line_price - array_sum( $base_taxes ), $tax_rates, false ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $base_taxes_total = array_sum( $base_taxes ); $modded_taxes_total = array_sum( $modded_taxes ); } else { $base_taxes_total = array_sum( array_map( 'wc_round_tax_total', $base_taxes ) ); $modded_taxes_total = array_sum( array_map( 'wc_round_tax_total', $modded_taxes ) ); } $return_price = NumberUtil::round( $line_price - $base_taxes_total + $modded_taxes_total, wc_get_price_decimals() ); } } } return apply_filters( 'woocommerce_get_price_including_tax', $return_price, $qty, $product ); } /** * For a given product, and optionally price/qty, work out the price with tax excluded, based on store settings. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @param array $args Optional arguments to pass product quantity and price. * @return float|string Price with tax excluded, or an empty string if price calculation failed. */ function wc_get_price_excluding_tax( $product, $args = array() ) { $args = wp_parse_args( $args, array( 'qty' => '', 'price' => '', ) ); $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price(); $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; if ( '' === $price ) { return ''; } elseif ( empty( $qty ) ) { return 0.0; } $line_price = $price * $qty; if ( $product->is_taxable() && wc_prices_include_tax() ) { $order = ArrayUtil::get_value_or_default( $args, 'order' ); $customer_id = $order ? $order->get_customer_id() : 0; if ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) || ! $customer_id ) { $tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); } else { $customer = wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Customer::class, $customer_id ); $tax_rates = WC_Tax::get_rates( $product->get_tax_class(), $customer ); } $remove_taxes = WC_Tax::calc_tax( $line_price, $tax_rates, true ); $return_price = $line_price - array_sum( $remove_taxes ); // Unrounded since we're dealing with tax inclusive prices. Matches logic in cart-totals class. @see adjust_non_base_location_price. } else { $return_price = $line_price; } return apply_filters( 'woocommerce_get_price_excluding_tax', $return_price, $qty, $product ); } /** * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @param array $args Optional arguments to pass product quantity and price. * @return float */ function wc_get_price_to_display( $product, $args = array() ) { $args = wp_parse_args( $args, array( 'qty' => 1, 'price' => $product->get_price(), ) ); $price = $args['price']; $qty = $args['qty']; return 'incl' === get_option( 'woocommerce_tax_display_shop' ) ? wc_get_price_including_tax( $product, array( 'qty' => $qty, 'price' => $price, ) ) : wc_get_price_excluding_tax( $product, array( 'qty' => $qty, 'price' => $price, ) ); } /** * Returns the product categories in a list. * * @param int $product_id Product ID. * @param string $sep (default: ', '). * @param string $before (default: ''). * @param string $after (default: ''). * @return string */ function wc_get_product_category_list( $product_id, $sep = ', ', $before = '', $after = '' ) { return get_the_term_list( $product_id, 'product_cat', $before, $sep, $after ); } /** * Returns the product tags in a list. * * @param int $product_id Product ID. * @param string $sep (default: ', '). * @param string $before (default: ''). * @param string $after (default: ''). * @return string */ function wc_get_product_tag_list( $product_id, $sep = ', ', $before = '', $after = '' ) { return get_the_term_list( $product_id, 'product_tag', $before, $sep, $after ); } /** * Callback for array filter to get visible only. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_visible( $product ) { return $product && is_a( $product, 'WC_Product' ) && $product->is_visible(); } /** * Callback for array filter to get visible grouped products only. * * @since 3.1.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_visible_grouped( $product ) { return $product && is_a( $product, 'WC_Product' ) && ( 'publish' === $product->get_status() || current_user_can( 'edit_product', $product->get_id() ) ); } /** * Callback for array filter to get products the user can edit only. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_editable( $product ) { return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'edit_product', $product->get_id() ); } /** * Callback for array filter to get products the user can view only. * * @since 3.4.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_readable( $product ) { return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'read_product', $product->get_id() ); } /** * Sort an array of products by a value. * * @since 3.0.0 * * @param array $products List of products to be ordered. * @param string $orderby Optional order criteria. * @param string $order Ascending or descending order. * * @return array */ function wc_products_array_orderby( $products, $orderby = 'date', $order = 'desc' ) { $orderby = strtolower( $orderby ); $order = strtolower( $order ); switch ( $orderby ) { case 'title': case 'id': case 'date': case 'modified': case 'menu_order': case 'price': usort( $products, 'wc_products_array_orderby_' . $orderby ); break; case 'none': break; default: shuffle( $products ); break; } if ( 'desc' === $order ) { $products = array_reverse( $products ); } return $products; } /** * Sort by title. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_title( $a, $b ) { return strcasecmp( $a->get_name(), $b->get_name() ); } /** * Sort by id. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_id( $a, $b ) { if ( $a->get_id() === $b->get_id() ) { return 0; } return ( $a->get_id() < $b->get_id() ) ? -1 : 1; } /** * Sort by date. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_date( $a, $b ) { if ( $a->get_date_created() === $b->get_date_created() ) { return 0; } return ( $a->get_date_created() < $b->get_date_created() ) ? -1 : 1; } /** * Sort by modified. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_modified( $a, $b ) { if ( $a->get_date_modified() === $b->get_date_modified() ) { return 0; } return ( $a->get_date_modified() < $b->get_date_modified() ) ? -1 : 1; } /** * Sort by menu order. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_menu_order( $a, $b ) { if ( $a->get_menu_order() === $b->get_menu_order() ) { return 0; } return ( $a->get_menu_order() < $b->get_menu_order() ) ? -1 : 1; } /** * Sort by price low to high. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_price( $a, $b ) { if ( $a->get_price() === $b->get_price() ) { return 0; } return ( $a->get_price() < $b->get_price() ) ? -1 : 1; } /** * Queue a product for syncing at the end of the request. * * @param int $product_id Product ID. */ function wc_deferred_product_sync( $product_id ) { global $wc_deferred_product_sync; if ( empty( $wc_deferred_product_sync ) ) { $wc_deferred_product_sync = array(); } $wc_deferred_product_sync[] = $product_id; } /** * See if the lookup table is being generated already. * * @since 3.6.0 * @return bool */ function wc_update_product_lookup_tables_is_running() { $table_updates_pending = WC()->queue()->search( array( 'status' => 'pending', 'group' => 'wc_update_product_lookup_tables', 'per_page' => 1, ) ); return (bool) count( $table_updates_pending ); } /** * Populate lookup table data for products. * * @since 3.6.0 */ function wc_update_product_lookup_tables() { global $wpdb; $is_cli = Constants::is_true( 'WP_CLI' ); if ( ! $is_cli ) { WC_Admin_Notices::add_notice( 'regenerating_lookup_table' ); } // Note that the table is not yet generated. update_option( 'woocommerce_product_lookup_table_is_generating', true ); // Make a row per product in lookup table. $wpdb->query( " INSERT IGNORE INTO {$wpdb->wc_product_meta_lookup} (`product_id`) SELECT posts.ID FROM {$wpdb->posts} posts WHERE posts.post_type IN ('product', 'product_variation') " ); // List of column names in the lookup table we need to populate. $columns = array( 'min_max_price', 'stock_quantity', 'sku', 'stock_status', 'average_rating', 'total_sales', 'downloadable', 'virtual', 'onsale', 'tax_class', 'tax_status', // When last column is updated, woocommerce_product_lookup_table_is_generating is updated. ); foreach ( $columns as $index => $column ) { if ( $is_cli ) { wc_update_product_lookup_tables_column( $column ); } else { WC()->queue()->schedule_single( time() + $index, 'wc_update_product_lookup_tables_column', array( 'column' => $column, ), 'wc_update_product_lookup_tables' ); } } // Rating counts are serialised so they have to be unserialised before populating the lookup table. if ( $is_cli ) { $rating_count_rows = $wpdb->get_results( " SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_wc_rating_count' AND meta_value != '' AND meta_value != 'a:0:{}' ", ARRAY_A ); wc_update_product_lookup_tables_rating_count( $rating_count_rows ); } else { WC()->queue()->schedule_single( time() + 10, 'wc_update_product_lookup_tables_rating_count_batch', array( 'offset' => 0, 'limit' => 50, ), 'wc_update_product_lookup_tables' ); } } /** * Populate lookup table column data. * * @since 3.6.0 * @param string $column Column name to set. */ function wc_update_product_lookup_tables_column( $column ) { if ( empty( $column ) ) { return; } global $wpdb; switch ( $column ) { case 'min_max_price': $wpdb->query( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table INNER JOIN ( SELECT lookup_table.product_id, MIN( meta_value+0 ) as min_price, MAX( meta_value+0 ) as max_price FROM {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' WHERE meta1.meta_value <> '' GROUP BY lookup_table.product_id ) as source on source.product_id = lookup_table.product_id SET lookup_table.min_price = source.min_price, lookup_table.max_price = source.max_price " ); break; case 'stock_quantity': $wpdb->query( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_manage_stock' LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_stock' SET lookup_table.stock_quantity = meta2.meta_value WHERE meta1.meta_value = 'yes' " ); break; case 'sku': case 'stock_status': case 'average_rating': case 'total_sales': case 'tax_class': case 'tax_status': if ( 'total_sales' === $column ) { $meta_key = 'total_sales'; } elseif ( 'average_rating' === $column ) { $meta_key = '_wc_average_rating'; } else { $meta_key = '_' . $column; } $column = esc_sql( $column ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta ON lookup_table.product_id = meta.post_id AND meta.meta_key = %s SET lookup_table.`{$column}` = meta.meta_value ", $meta_key ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared break; case 'downloadable': case 'virtual': $column = esc_sql( $column ); $meta_key = '_' . $column; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = %s SET lookup_table.`{$column}` = IF ( meta1.meta_value = 'yes', 1, 0 ) ", $meta_key ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared break; case 'onsale': $column = esc_sql( $column ); $decimals = absint( wc_get_price_decimals() ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_sale_price' SET lookup_table.`{$column}` = IF ( CAST( meta1.meta_value AS DECIMAL ) >= 0 AND CAST( meta2.meta_value AS CHAR ) != '' AND CAST( meta1.meta_value AS DECIMAL( 10, %d ) ) = CAST( meta2.meta_value AS DECIMAL( 10, %d ) ) , 1, 0 ) ", $decimals, $decimals ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared break; } // Final column - mark complete. if ( 'tax_status' === $column ) { delete_option( 'woocommerce_product_lookup_table_is_generating' ); } } add_action( 'wc_update_product_lookup_tables_column', 'wc_update_product_lookup_tables_column' ); /** * Populate rating count lookup table data for products. * * @since 3.6.0 * @param array $rows Rows of rating counts to update in lookup table. */ function wc_update_product_lookup_tables_rating_count( $rows ) { if ( ! $rows || ! is_array( $rows ) ) { return; } global $wpdb; foreach ( $rows as $row ) { $count = array_sum( (array) maybe_unserialize( $row['meta_value'] ) ); $wpdb->update( $wpdb->wc_product_meta_lookup, array( 'rating_count' => absint( $count ), ), array( 'product_id' => absint( $row['post_id'] ), ) ); } } /** * Populate a batch of rating count lookup table data for products. * * @since 3.6.2 * @param array $offset Offset to query. * @param array $limit Limit to query. */ function wc_update_product_lookup_tables_rating_count_batch( $offset = 0, $limit = 0 ) { global $wpdb; if ( ! $limit ) { return; } $rating_count_rows = $wpdb->get_results( $wpdb->prepare( " SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_wc_rating_count' AND meta_value != '' AND meta_value != 'a:0:{}' ORDER BY post_id ASC LIMIT %d, %d ", $offset, $limit ), ARRAY_A ); if ( $rating_count_rows ) { wc_update_product_lookup_tables_rating_count( $rating_count_rows ); WC()->queue()->schedule_single( time() + 1, 'wc_update_product_lookup_tables_rating_count_batch', array( 'offset' => $offset + $limit, 'limit' => $limit, ), 'wc_update_product_lookup_tables' ); } } add_action( 'wc_update_product_lookup_tables_rating_count_batch', 'wc_update_product_lookup_tables_rating_count_batch', 10, 2 ); ction add_sql_query_params( $query_args ) { global $wpdb; $order_product_lookup_table = self::get_db_table_name(); $this->add_time_period_sql_params( $query_args, $order_product_lookup_table ); $this->get_limit_sql_params( $query_args ); $this->add_order_by_sql_params( $query_args ); $included_products = $this->get_included_products( $query_args ); if ( $included_products ) { $this->add_from_sql_params( $query_args, 'outer', 'default_results.product_id' ); $this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" ); } else { $this->add_from_sql_params( $query_args, 'inner', "{$order_product_lookup_table}.product_id" ); } $included_variations = $this->get_included_variations( $query_args ); if ( $included_variations ) { $this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id IN ({$included_variations})" ); } $order_status_filter = $this->get_status_subquery( $query_args ); if ( $order_status_filter ) { $this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id" ); $this->subquery->add_sql_clause( 'where', "AND ( {$order_status_filter} )" ); } } /** * Maps ordering specified by the user to columns in the database/fields in the data. * * @param string $order_by Sorting criterion. * @return string */ protected function normalize_order_by( $order_by ) { if ( 'date' === $order_by ) { return self::get_db_table_name() . '.date_created'; } if ( 'product_name' === $order_by ) { return 'post_title'; } if ( 'sku' === $order_by ) { return 'meta_value'; } return $order_by; } /** * Enriches the product data with attributes specified by the extended_attributes. * * @param array $products_data Product data. * @param array $query_args Query parameters. */ protected function include_extended_info( &$products_data, $query_args ) { global $wpdb; $product_names = array(); foreach ( $products_data as $key => $product_data ) { $extended_info = new \ArrayObject(); if ( $query_args['extended_info'] ) { $product_id = $product_data['product_id']; $product = wc_get_product( $product_id ); // Product was deleted. if ( ! $product ) { if ( ! isset( $product_names[ $product_id ] ) ) { $product_names[ $product_id ] = $wpdb->get_var( $wpdb->prepare( "SELECT i.order_item_name FROM {$wpdb->prefix}woocommerce_order_items i, {$wpdb->prefix}woocommerce_order_itemmeta m WHERE i.order_item_id = m.order_item_id AND m.meta_key = '_product_id' AND m.meta_value = %s ORDER BY i.order_item_id DESC LIMIT 1", $product_id ) ); } /* translators: %s is product name */ $products_data[ $key ]['extended_info']['name'] = $product_names[ $product_id ] ? sprintf( __( '%s (Deleted)', 'woocommerce' ), $product_names[ $product_id ] ) : __( '(Deleted)', 'woocommerce' ); continue; } $extended_attributes = apply_filters( 'woocommerce_rest_reports_products_extended_attributes', $this->extended_attributes, $product_data ); foreach ( $extended_attributes as $extended_attribute ) { if ( 'variations' === $extended_attribute ) { if ( ! $product->is_type( 'variable' ) ) { continue; } $function = 'get_children'; } else { $function = 'get_' . $extended_attribute; } if ( is_callable( array( $product, $function ) ) ) { $value = $product->{$function}(); $extended_info[ $extended_attribute ] = $value; } } // If there is no set low_stock_amount, use the one in user settings. if ( '' === $extended_info['low_stock_amount'] ) { $extended_info['low_stock_amount'] = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); } $extended_info = $this->cast_numbers( $extended_info ); } $products_data[ $key ]['extended_info'] = $extended_info; } } /** * Returns the report data based on parameters supplied by the user. * * @param array $query_args Query parameters. * @return stdClass|WP_Error Data. */ public function get_data( $query_args ) { global $wpdb; $table_name = self::get_db_table_name(); // These defaults are only partially applied when used via REST API, as that has its own defaults. $defaults = array( 'per_page' => get_option( 'posts_per_page' ), 'page' => 1, 'order' => 'DESC', 'orderby' => 'date', 'before' => TimeInterval::default_before(), 'after' => TimeInterval::default_after(), 'fields' => '*', 'category_includes' => array(), 'product_includes' => array(), 'extended_info' => false, ); $query_args = wp_parse_args( $query_args, $defaults ); $this->normalize_timezones( $query_args, $defaults ); /* * We need to get the cache key here because * parent::update_intervals_sql_params() modifies $query_args. */ $cache_key = $this->get_cache_key( $query_args ); $data = $this->get_cached_data( $cache_key ); if ( false === $data ) { $this->initialize_queries(); $data = (object) array( 'data' => array(), 'total' => 0, 'pages' => 0, 'page_no' => 0, ); $selections = $this->selected_columns( $query_args ); $included_products = $this->get_included_products_array( $query_args ); $params = $this->get_limit_params( $query_args ); $this->add_sql_query_params( $query_args ); if ( count( $included_products ) > 0 ) { $filtered_products = array_diff( $included_products, array( '-1' ) ); $total_results = count( $filtered_products ); $total_pages = (int) ceil( $total_results / $params['per_page'] ); if ( 'date' === $query_args['orderby'] ) { $selections .= ", {$table_name}.date_created"; } $fields = $this->get_fields( $query_args ); $join_selections = $this->format_join_selections( $fields, array( 'product_id' ) ); $ids_table = $this->get_ids_table( $included_products, 'product_id' ); $this->subquery->clear_sql_clause( 'select' ); $this->subquery->add_sql_clause( 'select', $selections ); $this->add_sql_clause( 'select', $join_selections ); $this->add_sql_clause( 'from', '(' ); $this->add_sql_clause( 'from', $this->subquery->get_query_statement() ); $this->add_sql_clause( 'from', ") AS {$table_name}" ); $this->add_sql_clause( 'right_join', "RIGHT JOIN ( {$ids_table} ) AS default_results ON default_results.product_id = {$table_name}.product_id" ); $this->add_sql_clause( 'where', 'AND default_results.product_id != -1' ); $products_query = $this->get_query_statement(); } else { $count_query = "SELECT COUNT(*) FROM ( {$this->subquery->get_query_statement()} ) AS tt"; $db_records_count = (int) $wpdb->get_var( $count_query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared ); $total_results = $db_records_count; $total_pages = (int) ceil( $db_records_count / $params['per_page'] ); if ( ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) ) { return $data; } $this->subquery->clear_sql_clause( 'select' ); $this->subquery->add_sql_clause( 'select', $selections ); $this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) ); $this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) ); $products_query = $this->subquery->get_query_statement(); } $product_data = $wpdb->get_results( $products_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared ARRAY_A ); if ( null === $product_data ) { return $data; } $product_data = array_map( array( $this, 'cast_numbers' ), $product_data ); $data = (object) array( 'data' => $product_data, 'total' => $total_results, 'pages' => $total_pages, 'page_no' => (int) $query_args['page'], ); $this->set_cached_data( $cache_key, $data ); } $this->include_extended_info( $data->data, $query_args ); return $data; } /** * Create or update an entry in the wc_admin_order_product_lookup table for an order. * * @since 3.5.0 * @param int $order_id Order ID. * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success. */ public static function sync_order_products( $order_id ) { global $wpdb; $order = wc_get_order( $order_id ); if ( ! $order ) { return -1; } $table_name = self::get_db_table_name(); $existing_items = $wpdb->get_col( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT order_item_id FROM {$table_name} WHERE order_id = %d", $order_id ) ); $existing_items = array_flip( $existing_items ); $order_items = $order->get_items(); $num_updated = 0; $decimals = wc_get_price_decimals(); $round_tax = 'no' === get_option( 'woocommerce_tax_round_at_subtotal' ); foreach ( $order_items as $order_item ) { $order_item_id = $order_item->get_id(); unset( $existing_items[ $order_item_id ] ); $product_qty = $order_item->get_quantity( 'edit' ); $shipping_amount = $order->get_item_shipping_amount( $order_item ); $shipping_tax_amount = $order->get_item_shipping_tax_amount( $order_item ); $coupon_amount = $order->get_item_coupon_amount( $order_item ); // Skip line items without changes to product quantity. if ( ! $product_qty ) { $num_updated++; continue; } // Tax amount. $tax_amount = 0; $order_taxes = $order->get_taxes(); $tax_data = $order_item->get_taxes(); foreach ( $order_taxes as $tax_item ) { $tax_item_id = $tax_item->get_rate_id(); $tax_amount += isset( $tax_data['total'][ $tax_item_id ] ) ? (float) $tax_data['total'][ $tax_item_id ] : 0; } $net_revenue = round( $order_item->get_total( 'edit' ), $decimals ); if ( $round_tax ) { $tax_amount = round( $tax_amount, $decimals ); } $result = $wpdb->replace( self::get_db_table_name(), array( 'order_item_id' => $order_item_id, 'order_id' => $order->get_id(), 'product_id' => wc_get_order_item_meta( $order_item_id, '_product_id' ), 'variation_id' => wc_get_order_item_meta( $order_item_id, '_variation_id' ), 'customer_id' => $order->get_report_customer_id(), 'product_qty' => $product_qty, 'product_net_revenue' => $net_revenue, 'date_created' => $order->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ), 'coupon_amount' => $coupon_amount, 'tax_amount' => $tax_amount, 'shipping_amount' => $shipping_amount, 'shipping_tax_amount' => $shipping_tax_amount, // @todo Can this be incorrect if modified by filters? 'product_gross_revenue' => $net_revenue + $tax_amount + $shipping_amount + $shipping_tax_amount, ), array( '%d', // order_item_id. '%d', // order_id. '%d', // product_id. '%d', // variation_id. '%d', // customer_id. '%d', // product_qty. '%f', // product_net_revenue. '%s', // date_created. '%f', // coupon_amount. '%f', // tax_amount. '%f', // shipping_amount. '%f', // shipping_tax_amount. '%f', // product_gross_revenue. ) ); // WPCS: cache ok, DB call ok, unprepared SQL ok. /** * Fires when product's reports are updated. * * @param int $order_item_id Order Item ID. * @param int $order_id Order ID. */ do_action( 'woocommerce_analytics_update_product', $order_item_id, $order->get_id() ); // Sum the rows affected. Using REPLACE can affect 2 rows if the row already exists. $num_updated += 2 === intval( $result ) ? 1 : intval( $result ); } if ( ! empty( $existing_items ) ) { $existing_items = array_flip( $existing_items ); $format = array_fill( 0, count( $existing_items ), '%d' ); $format = implode( ',', $format ); array_unshift( $existing_items, $order_id ); $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared "DELETE FROM {$table_name} WHERE order_id = %d AND order_item_id in ({$format})", $existing_items ) ); } return ( count( $order_items ) === $num_updated ); } /** * Clean products data when an order is deleted. * * @param int $order_id Order ID. */ public static function sync_on_order_delete( $order_id ) { global $wpdb; $wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) ); /** * Fires when product's reports are removed from database. * * @param int $product_id Product ID. * @param int $order_id Order ID. */ do_action( 'woocommerce_analytics_delete_product', 0, $order_id ); ReportsCache::invalidate(); } /** * Initialize query objects. */ protected function initialize_queries() { $this->clear_all_clauses(); $this->subquery = new SqlQuery( $this->context . '_subquery' ); $this->subquery->add_sql_clause( 'select', 'product_id' ); $this->subquery->add_sql_clause( 'from', self::get_db_table_name() ); $this->subquery->add_sql_clause( 'group_by', 'product_id' ); } }