woothemes/woocommerce

View on GitHub
includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php

Summary

Maintainability
F
3 days
Test Coverage
<?php
/**
 * Abstract Rest CRUD Controller Class
 *
 * @class    WC_REST_CRUD_Controller
 * @package WooCommerce\RestApi
 * @version  3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * WC_REST_CRUD_Controller class.
 *
 * @extends WC_REST_Posts_Controller
 */
abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller {

    /**
     * Endpoint namespace.
     *
     * @var string
     */
    protected $namespace = 'wc/v2';

    /**
     * If object is hierarchical.
     *
     * @var bool
     */
    protected $hierarchical = false;

    /**
     * Get object.
     *
     * @param  int $id Object ID.
     * @return object WC_Data object or WP_Error object.
     */
    protected function get_object( $id ) {
        // translators: %s: Class method name.
        return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) );
    }

    /**
     * Check if a given request has access to read an item.
     *
     * @param  WP_REST_Request $request Full details about the request.
     * @return WP_Error|boolean
     */
    public function get_item_permissions_check( $request ) {
        $object = $this->get_object( (int) $request['id'] );

        if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) {
            return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
        }

        return true;
    }

    /**
     * Check if a given request has access to update an item.
     *
     * @param  WP_REST_Request $request Full details about the request.
     * @return WP_Error|boolean
     */
    public function update_item_permissions_check( $request ) {
        $object = $this->get_object( (int) $request['id'] );

        if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) {
            return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
        }

        return true;
    }

    /**
     * Check if a given request has access to delete an item.
     *
     * @param  WP_REST_Request $request Full details about the request.
     * @return bool|WP_Error
     */
    public function delete_item_permissions_check( $request ) {
        $object = $this->get_object( (int) $request['id'] );

        if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) {
            return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
        }

        return true;
    }

    /**
     * Get object permalink.
     *
     * @param  object $object Object.
     * @return string
     */
    protected function get_permalink( $object ) {
        return '';
    }

    /**
     * Prepares the object for the REST response.
     *
     * @since  3.0.0
     * @param  WC_Data         $object  Object data.
     * @param  WP_REST_Request $request Request object.
     * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
     */
    protected function prepare_object_for_response( $object, $request ) {
        // translators: %s: Class method name.
        return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) );
    }

    /**
     * Prepares one object for create or update operation.
     *
     * @since  3.0.0
     * @param  WP_REST_Request $request Request object.
     * @param  bool            $creating If is creating a new object.
     * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure.
     */
    protected function prepare_object_for_database( $request, $creating = false ) {
        // translators: %s: Class method name.
        return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) );
    }

    /**
     * Get a single item.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function get_item( $request ) {
        $object = $this->get_object( (int) $request['id'] );

        if ( ! $object || 0 === $object->get_id() ) {
            return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
        }

        $data     = $this->prepare_object_for_response( $object, $request );
        $response = rest_ensure_response( $data );

        if ( $this->public ) {
            $response->link_header( 'alternate', $this->get_permalink( $object ), array( 'type' => 'text/html' ) );
        }

        return $response;
    }

    /**
     * Save an object data.
     *
     * @since  3.0.0
     * @param  WP_REST_Request $request  Full details about the request.
     * @param  bool            $creating If is creating a new object.
     * @return WC_Data|WP_Error
     */
    protected function save_object( $request, $creating = false ) {
        try {
            $object = $this->prepare_object_for_database( $request, $creating );

            if ( is_wp_error( $object ) ) {
                return $object;
            }

            $object->save();

            return $this->get_object( $object->get_id() );
        } catch ( WC_Data_Exception $e ) {
            return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
        } catch ( WC_REST_Exception $e ) {
            return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
        }
    }

    /**
     * Create a single item.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function create_item( $request ) {
        if ( ! empty( $request['id'] ) ) {
            /* translators: %s: post type */
            return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
        }

        $object = $this->save_object( $request, true );

        if ( is_wp_error( $object ) ) {
            return $object;
        }

        try {
            $this->update_additional_fields_for_object( $object, $request );

            /**
             * Fires after a single object is created or updated via the REST API.
             *
             * @param WC_Data         $object    Inserted object.
             * @param WP_REST_Request $request   Request object.
             * @param boolean         $creating  True when creating object, false when updating.
             */
            do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, true );
        } catch ( WC_Data_Exception $e ) {
            $object->delete();
            return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
        } catch ( WC_REST_Exception $e ) {
            $object->delete();
            return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
        }

        $request->set_param( 'context', 'edit' );
        $response = $this->prepare_object_for_response( $object, $request );
        $response = rest_ensure_response( $response );
        $response->set_status( 201 );
        $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ) );

        return $response;
    }

    /**
     * Update a single post.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function update_item( $request ) {
        $object = $this->get_object( (int) $request['id'] );

        if ( ! $object || 0 === $object->get_id() ) {
            return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 400 ) );
        }

        $object = $this->save_object( $request, false );

        if ( is_wp_error( $object ) ) {
            return $object;
        }

        try {
            $this->update_additional_fields_for_object( $object, $request );

            /**
             * Fires after a single object is created or updated via the REST API.
             *
             * @param WC_Data         $object    Inserted object.
             * @param WP_REST_Request $request   Request object.
             * @param boolean         $creating  True when creating object, false when updating.
             */
            do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, false );
        } catch ( WC_Data_Exception $e ) {
            return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
        } catch ( WC_REST_Exception $e ) {
            return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
        }

        $request->set_param( 'context', 'edit' );
        $response = $this->prepare_object_for_response( $object, $request );
        return rest_ensure_response( $response );
    }

    /**
     * Prepare objects query.
     *
     * @since  3.0.0
     * @param  WP_REST_Request $request Full details about the request.
     * @return array
     */
    protected function prepare_objects_query( $request ) {
        $args                        = array();
        $args['offset']              = $request['offset'];
        $args['order']               = $request['order'];
        $args['orderby']             = $request['orderby'];
        $args['paged']               = $request['page'];
        $args['post__in']            = $request['include'];
        $args['post__not_in']        = $request['exclude'];
        $args['posts_per_page']      = $request['per_page'];
        $args['name']                = $request['slug'];
        $args['post_parent__in']     = $request['parent'];
        $args['post_parent__not_in'] = $request['parent_exclude'];
        $args['s']                   = $request['search'];
        $args['fields']              = $this->get_fields_for_response( $request );

        if ( 'date' === $args['orderby'] ) {
            $args['orderby'] = 'date ID';
        }

        $args['date_query'] = array();
        // Set before into date query. Date query must be specified as an array of an array.
        if ( isset( $request['before'] ) ) {
            $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( $request['after'] ) ) {
            $args['date_query'][0]['after'] = $request['after'];
        }

        // Force the post_type argument, since it's not a user input variable.
        $args['post_type'] = $this->post_type;

        /**
         * Filter the query arguments for a request.
         *
         * Enables adding extra arguments or setting defaults for a post
         * collection request.
         *
         * @param array           $args    Key value array of query var to query value.
         * @param WP_REST_Request $request The request used.
         */
        $args = apply_filters( "woocommerce_rest_{$this->post_type}_object_query", $args, $request );

        return $this->prepare_items_query( $args, $request );
    }

    /**
     * Get objects.
     *
     * @since  3.0.0
     * @param  array $query_args Query args.
     * @return array
     */
    protected function get_objects( $query_args ) {
        $query  = new WP_Query();
        $result = $query->query( $query_args );

        $total_posts = $query->found_posts;
        if ( $total_posts < 1 ) {
            // Out-of-bounds, run the query again without LIMIT for total count.
            unset( $query_args['paged'] );
            $count_query = new WP_Query();
            $count_query->query( $query_args );
            $total_posts = $count_query->found_posts;
        }

        return array(
            'objects' => array_filter( array_map( array( $this, 'get_object' ), $result ) ),
            'total'   => (int) $total_posts,
            'pages'   => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ),
        );
    }

    /**
     * Get a collection of posts.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function get_items( $request ) {
        $query_args    = $this->prepare_objects_query( $request );
        $query_results = $this->get_objects( $query_args );

        $objects = array();
        foreach ( $query_results['objects'] as $object ) {
            if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) {
                continue;
            }

            $data = $this->prepare_object_for_response( $object, $request );
            $objects[] = $this->prepare_response_for_collection( $data );
        }

        $page      = (int) $query_args['paged'];
        $max_pages = $query_results['pages'];

        $response = rest_ensure_response( $objects );
        $response->header( 'X-WP-Total', $query_results['total'] );
        $response->header( 'X-WP-TotalPages', (int) $max_pages );

        $base          = $this->rest_base;
        $attrib_prefix = '(?P<';
        if ( strpos( $base, $attrib_prefix ) !== false ) {
            $attrib_names = array();
            preg_match( '/\(\?P<[^>]+>.*\)/', $base, $attrib_names, PREG_OFFSET_CAPTURE );
            foreach ( $attrib_names as $attrib_name_match ) {
                $beginning_offset = strlen( $attrib_prefix );
                $attrib_name_end  = strpos( $attrib_name_match[0], '>', $attrib_name_match[1] );
                $attrib_name      = substr( $attrib_name_match[0], $beginning_offset, $attrib_name_end - $beginning_offset );
                if ( isset( $request[ $attrib_name ] ) ) {
                    $base  = str_replace( "(?P<$attrib_name>[\d]+)", $request[ $attrib_name ], $base );
                }
            }
        }
        $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $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;
    }

    /**
     * Delete a single item.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function delete_item( $request ) {
        $force  = (bool) $request['force'];
        $object = $this->get_object( (int) $request['id'] );
        $result = false;

        if ( ! $object || 0 === $object->get_id() ) {
            return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
        }

        $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) );

        /**
         * Filter whether an object is trashable.
         *
         * Return false to disable trash support for the object.
         *
         * @param boolean $supports_trash Whether the object type support trashing.
         * @param WC_Data $object         The object being considered for trashing support.
         */
        $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object );

        if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) {
            /* translators: %s: post type */
            return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) );
        }

        $request->set_param( 'context', 'edit' );
        $response = $this->prepare_object_for_response( $object, $request );

        // If we're forcing, then delete permanently.
        if ( $force ) {
            $object->delete( true );
            $result = 0 === $object->get_id();
        } else {
            // If we don't support trashing for this type, error out.
            if ( ! $supports_trash ) {
                /* translators: %s: post type */
                return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) );
            }

            // Otherwise, only trash if we haven't already.
            if ( is_callable( array( $object, 'get_status' ) ) ) {
                if ( 'trash' === $object->get_status() ) {
                    /* translators: %s: post type */
                    return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) );
                }

                $object->delete();
                $result = 'trash' === $object->get_status();
            }
        }

        if ( ! $result ) {
            /* translators: %s: post type */
            return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) );
        }

        /**
         * Fires after a single object is deleted or trashed via the REST API.
         *
         * @param WC_Data          $object   The deleted or trashed object.
         * @param WP_REST_Response $response The response data.
         * @param WP_REST_Request  $request  The request sent to the API.
         */
        do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request );

        return $response;
    }

    /**
     * Get fields for an object if getter is defined.
     *
     * @param object $object  Object we are fetching response for.
     * @param string $context Context of the request. Can be `view` or `edit`.
     * @param array  $fields  List of fields to fetch.
     * @return array Data fetched from getters.
     */
    public function fetch_fields_using_getters( $object, $context, $fields ) {
        $data = array();
        foreach ( $fields as $field ) {
            if ( method_exists( $this, "api_get_$field" ) ) {
                $data[ $field ] = $this->{"api_get_$field"}( $object, $context );
            }
        }
        return $data;
    }

    /**
     * Prepare links for the request.
     *
     * @param WC_Data         $object  Object data.
     * @param WP_REST_Request $request Request object.
     * @return array                   Links for the given post.
     */
    protected function prepare_links( $object, $request ) {
        $links = array(
            'self' => array(
                'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ),
            ),
            'collection' => array(
                'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
            ),
        );

        return $links;
    }

    /**
     * Get the query params for collections of attachments.
     *
     * @return array
     */
    public function get_collection_params() {
        $params                       = array();
        $params['context']            = $this->get_context_param();
        $params['context']['default'] = 'view';

        $params['page'] = array(
            'description'        => __( 'Current page of the collection.', 'woocommerce' ),
            'type'               => 'integer',
            'default'            => 1,
            'sanitize_callback'  => 'absint',
            'validate_callback'  => 'rest_validate_request_arg',
            'minimum'            => 1,
        );
        $params['per_page'] = array(
            'description'        => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
            'type'               => 'integer',
            'default'            => 10,
            'minimum'            => 1,
            'maximum'            => 100,
            'sanitize_callback'  => 'absint',
            'validate_callback'  => 'rest_validate_request_arg',
        );
        $params['search'] = array(
            'description'        => __( 'Limit results to those matching a string.', 'woocommerce' ),
            'type'               => 'string',
            'sanitize_callback'  => 'sanitize_text_field',
            'validate_callback'  => 'rest_validate_request_arg',
        );
        $params['after'] = array(
            'description'        => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
            'type'               => 'string',
            'format'             => 'date-time',
            'validate_callback'  => 'rest_validate_request_arg',
        );
        $params['before'] = array(
            'description'        => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
            'type'               => 'string',
            'format'             => 'date-time',
            'validate_callback'  => 'rest_validate_request_arg',
        );
        $params['exclude'] = array(
            'description'       => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
            'type'              => 'array',
            'items'             => array(
                'type'          => 'integer',
            ),
            'default'           => array(),
            'sanitize_callback' => 'wp_parse_id_list',
        );
        $params['include'] = array(
            'description'       => __( 'Limit result set to specific ids.', 'woocommerce' ),
            'type'              => 'array',
            'items'             => array(
                'type'          => 'integer',
            ),
            'default'           => array(),
            'sanitize_callback' => 'wp_parse_id_list',
        );
        $params['offset'] = array(
            'description'        => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
            'type'               => 'integer',
            'sanitize_callback'  => 'absint',
            'validate_callback'  => 'rest_validate_request_arg',
        );
        $params['order'] = array(
            'description'        => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
            'type'               => 'string',
            'default'            => 'desc',
            'enum'               => array( 'asc', 'desc' ),
            'validate_callback'  => 'rest_validate_request_arg',
        );
        $params['orderby'] = array(
            'description'        => __( 'Sort collection by object attribute.', 'woocommerce' ),
            'type'               => 'string',
            'default'            => 'date',
            'enum'               => array(
                'date',
                'id',
                'include',
                'title',
                'slug',
                'modified',
            ),
            'validate_callback'  => 'rest_validate_request_arg',
        );

        if ( $this->hierarchical ) {
            $params['parent'] = array(
                'description'       => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ),
                'type'              => 'array',
                'items'             => array(
                    'type'          => 'integer',
                ),
                'sanitize_callback' => 'wp_parse_id_list',
                'default'           => array(),
            );
            $params['parent_exclude'] = array(
                'description'       => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ),
                'type'              => 'array',
                'items'             => array(
                    'type'          => 'integer',
                ),
                'sanitize_callback' => 'wp_parse_id_list',
                'default'           => array(),
            );
        }

        /**
         * Filter collection parameters for the posts controller.
         *
         * The dynamic part of the filter `$this->post_type` refers to the post
         * type slug for the controller.
         *
         * This filter registers the collection parameter, but does not map the
         * collection parameter to an internal WP_Query parameter. Use the
         * `rest_{$this->post_type}_query` filter to set WP_Query parameters.
         *
         * @param array        $query_params JSON Schema-formatted collection parameters.
         * @param WP_Post_Type $post_type    Post type object.
         */
        return apply_filters( "rest_{$this->post_type}_collection_params", $params, $this->post_type );
    }
}