home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress2 / wp-includes / rest-api / endpoints / class-wp-rest-posts-controller.php < prev    next >
Encoding:
PHP Script  |  2017-10-23  |  72.2 KB  |  2,299 lines

  1. <?php
  2. /**
  3.  * REST API: WP_REST_Posts_Controller class
  4.  *
  5.  * @package WordPress
  6.  * @subpackage REST_API
  7.  * @since 4.7.0
  8.  */
  9.  
  10. /**
  11.  * Core class to access posts via the REST API.
  12.  *
  13.  * @since 4.7.0
  14.  *
  15.  * @see WP_REST_Controller
  16.  */
  17. class WP_REST_Posts_Controller extends WP_REST_Controller {
  18.  
  19.     /**
  20.      * Post type.
  21.      *
  22.      * @since 4.7.0
  23.      * @var string
  24.      */
  25.     protected $post_type;
  26.  
  27.     /**
  28.      * Instance of a post meta fields object.
  29.      *
  30.      * @since 4.7.0
  31.      * @var WP_REST_Post_Meta_Fields
  32.      */
  33.     protected $meta;
  34.  
  35.     /**
  36.      * Constructor.
  37.      *
  38.      * @since 4.7.0
  39.      *
  40.      * @param string $post_type Post type.
  41.      */
  42.     public function __construct( $post_type ) {
  43.         $this->post_type = $post_type;
  44.         $this->namespace = 'wp/v2';
  45.         $obj = get_post_type_object( $post_type );
  46.         $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
  47.  
  48.         $this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
  49.     }
  50.  
  51.     /**
  52.      * Registers the routes for the objects of the controller.
  53.      *
  54.      * @since 4.7.0
  55.      *
  56.      * @see register_rest_route()
  57.      */
  58.     public function register_routes() {
  59.  
  60.         register_rest_route( $this->namespace, '/' . $this->rest_base, array(
  61.             array(
  62.                 'methods'             => WP_REST_Server::READABLE,
  63.                 'callback'            => array( $this, 'get_items' ),
  64.                 'permission_callback' => array( $this, 'get_items_permissions_check' ),
  65.                 'args'                => $this->get_collection_params(),
  66.             ),
  67.             array(
  68.                 'methods'             => WP_REST_Server::CREATABLE,
  69.                 'callback'            => array( $this, 'create_item' ),
  70.                 'permission_callback' => array( $this, 'create_item_permissions_check' ),
  71.                 'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
  72.             ),
  73.             'schema' => array( $this, 'get_public_item_schema' ),
  74.         ) );
  75.  
  76.         $schema = $this->get_item_schema();
  77.         $get_item_args = array(
  78.             'context'  => $this->get_context_param( array( 'default' => 'view' ) ),
  79.         );
  80.         if ( isset( $schema['properties']['password'] ) ) {
  81.             $get_item_args['password'] = array(
  82.                 'description' => __( 'The password for the post if it is password protected.' ),
  83.                 'type'        => 'string',
  84.             );
  85.         }
  86.         register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
  87.             'args' => array(
  88.                 'id' => array(
  89.                     'description' => __( 'Unique identifier for the object.' ),
  90.                     'type'        => 'integer',
  91.                 ),
  92.             ),
  93.             array(
  94.                 'methods'             => WP_REST_Server::READABLE,
  95.                 'callback'            => array( $this, 'get_item' ),
  96.                 'permission_callback' => array( $this, 'get_item_permissions_check' ),
  97.                 'args'                => $get_item_args,
  98.             ),
  99.             array(
  100.                 'methods'             => WP_REST_Server::EDITABLE,
  101.                 'callback'            => array( $this, 'update_item' ),
  102.                 'permission_callback' => array( $this, 'update_item_permissions_check' ),
  103.                 'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  104.             ),
  105.             array(
  106.                 'methods'             => WP_REST_Server::DELETABLE,
  107.                 'callback'            => array( $this, 'delete_item' ),
  108.                 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
  109.                 'args'                => array(
  110.                     'force' => array(
  111.                         'type'        => 'boolean',
  112.                         'default'     => false,
  113.                         'description' => __( 'Whether to bypass trash and force deletion.' ),
  114.                     ),
  115.                 ),
  116.             ),
  117.             'schema' => array( $this, 'get_public_item_schema' ),
  118.         ) );
  119.     }
  120.  
  121.     /**
  122.      * Checks if a given request has access to read posts.
  123.      *
  124.      * @since 4.7.0
  125.      *
  126.      * @param  WP_REST_Request $request Full details about the request.
  127.      * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
  128.      */
  129.     public function get_items_permissions_check( $request ) {
  130.  
  131.         $post_type = get_post_type_object( $this->post_type );
  132.  
  133.         if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
  134.             return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit posts in this post type.' ), array( 'status' => rest_authorization_required_code() ) );
  135.         }
  136.  
  137.         return true;
  138.     }
  139.  
  140.     /**
  141.      * Retrieves a collection of posts.
  142.      *
  143.      * @since 4.7.0
  144.      *
  145.      * @param WP_REST_Request $request Full details about the request.
  146.      * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  147.      */
  148.     public function get_items( $request ) {
  149.  
  150.         // Ensure a search string is set in case the orderby is set to 'relevance'.
  151.         if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) ) {
  152.             return new WP_Error( 'rest_no_search_term_defined', __( 'You need to define a search term to order by relevance.' ), array( 'status' => 400 ) );
  153.         }
  154.  
  155.         // Ensure an include parameter is set in case the orderby is set to 'include'.
  156.         if ( ! empty( $request['orderby'] ) && 'include' === $request['orderby'] && empty( $request['include'] ) ) {
  157.             return new WP_Error( 'rest_orderby_include_missing_include', __( 'You need to define an include parameter to order by include.' ), array( 'status' => 400 ) );
  158.         }
  159.  
  160.         // Retrieve the list of registered collection query parameters.
  161.         $registered = $this->get_collection_params();
  162.         $args = array();
  163.  
  164.         /*
  165.          * This array defines mappings between public API query parameters whose
  166.          * values are accepted as-passed, and their internal WP_Query parameter
  167.          * name equivalents (some are the same). Only values which are also
  168.          * present in $registered will be set.
  169.          */
  170.         $parameter_mappings = array(
  171.             'author'         => 'author__in',
  172.             'author_exclude' => 'author__not_in',
  173.             'exclude'        => 'post__not_in',
  174.             'include'        => 'post__in',
  175.             'menu_order'     => 'menu_order',
  176.             'offset'         => 'offset',
  177.             'order'          => 'order',
  178.             'orderby'        => 'orderby',
  179.             'page'           => 'paged',
  180.             'parent'         => 'post_parent__in',
  181.             'parent_exclude' => 'post_parent__not_in',
  182.             'search'         => 's',
  183.             'slug'           => 'post_name__in',
  184.             'status'         => 'post_status',
  185.         );
  186.  
  187.         /*
  188.          * For each known parameter which is both registered and present in the request,
  189.          * set the parameter's value on the query $args.
  190.          */
  191.         foreach ( $parameter_mappings as $api_param => $wp_param ) {
  192.             if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
  193.                 $args[ $wp_param ] = $request[ $api_param ];
  194.             }
  195.         }
  196.  
  197.         // Check for & assign any parameters which require special handling or setting.
  198.         $args['date_query'] = array();
  199.  
  200.         // Set before into date query. Date query must be specified as an array of an array.
  201.         if ( isset( $registered['before'], $request['before'] ) ) {
  202.             $args['date_query'][0]['before'] = $request['before'];
  203.         }
  204.  
  205.         // Set after into date query. Date query must be specified as an array of an array.
  206.         if ( isset( $registered['after'], $request['after'] ) ) {
  207.             $args['date_query'][0]['after'] = $request['after'];
  208.         }
  209.  
  210.         // Ensure our per_page parameter overrides any provided posts_per_page filter.
  211.         if ( isset( $registered['per_page'] ) ) {
  212.             $args['posts_per_page'] = $request['per_page'];
  213.         }
  214.  
  215.         if ( isset( $registered['sticky'], $request['sticky'] ) ) {
  216.             $sticky_posts = get_option( 'sticky_posts', array() );
  217.             if ( ! is_array( $sticky_posts ) ) {
  218.                 $sticky_posts = array();
  219.             }
  220.             if ( $request['sticky'] ) {
  221.                 /*
  222.                  * As post__in will be used to only get sticky posts,
  223.                  * we have to support the case where post__in was already
  224.                  * specified.
  225.                  */
  226.                 $args['post__in'] = $args['post__in'] ? array_intersect( $sticky_posts, $args['post__in'] ) : $sticky_posts;
  227.  
  228.                 /*
  229.                  * If we intersected, but there are no post ids in common,
  230.                  * WP_Query won't return "no posts" for post__in = array()
  231.                  * so we have to fake it a bit.
  232.                  */
  233.                 if ( ! $args['post__in'] ) {
  234.                     $args['post__in'] = array( 0 );
  235.                 }
  236.             } elseif ( $sticky_posts ) {
  237.                 /*
  238.                  * As post___not_in will be used to only get posts that
  239.                  * are not sticky, we have to support the case where post__not_in
  240.                  * was already specified.
  241.                  */
  242.                 $args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts );
  243.             }
  244.         }
  245.  
  246.         // Force the post_type argument, since it's not a user input variable.
  247.         $args['post_type'] = $this->post_type;
  248.  
  249.         /**
  250.          * Filters the query arguments for a request.
  251.          *
  252.          * Enables adding extra arguments or setting defaults for a post collection request.
  253.          *
  254.          * @since 4.7.0
  255.          *
  256.          * @link https://developer.wordpress.org/reference/classes/wp_query/
  257.          *
  258.          * @param array           $args    Key value array of query var to query value.
  259.          * @param WP_REST_Request $request The request used.
  260.          */
  261.         $args = apply_filters( "rest_{$this->post_type}_query", $args, $request );
  262.         $query_args = $this->prepare_items_query( $args, $request );
  263.  
  264.         $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
  265.  
  266.         foreach ( $taxonomies as $taxonomy ) {
  267.             $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
  268.             $tax_exclude = $base . '_exclude';
  269.  
  270.             if ( ! empty( $request[ $base ] ) ) {
  271.                 $query_args['tax_query'][] = array(
  272.                     'taxonomy'         => $taxonomy->name,
  273.                     'field'            => 'term_id',
  274.                     'terms'            => $request[ $base ],
  275.                     'include_children' => false,
  276.                 );
  277.             }
  278.  
  279.             if ( ! empty( $request[ $tax_exclude ] ) ) {
  280.                 $query_args['tax_query'][] = array(
  281.                     'taxonomy'         => $taxonomy->name,
  282.                     'field'            => 'term_id',
  283.                     'terms'            => $request[ $tax_exclude ],
  284.                     'include_children' => false,
  285.                     'operator'         => 'NOT IN',
  286.                 );
  287.             }
  288.         }
  289.  
  290.         $posts_query  = new WP_Query();
  291.         $query_result = $posts_query->query( $query_args );
  292.  
  293.         // Allow access to all password protected posts if the context is edit.
  294.         if ( 'edit' === $request['context'] ) {
  295.             add_filter( 'post_password_required', '__return_false' );
  296.         }
  297.  
  298.         $posts = array();
  299.  
  300.         foreach ( $query_result as $post ) {
  301.             if ( ! $this->check_read_permission( $post ) ) {
  302.                 continue;
  303.             }
  304.  
  305.             $data    = $this->prepare_item_for_response( $post, $request );
  306.             $posts[] = $this->prepare_response_for_collection( $data );
  307.         }
  308.  
  309.         // Reset filter.
  310.         if ( 'edit' === $request['context'] ) {
  311.             remove_filter( 'post_password_required', '__return_false' );
  312.         }
  313.  
  314.         $page = (int) $query_args['paged'];
  315.         $total_posts = $posts_query->found_posts;
  316.  
  317.         if ( $total_posts < 1 ) {
  318.             // Out-of-bounds, run the query again without LIMIT for total count.
  319.             unset( $query_args['paged'] );
  320.  
  321.             $count_query = new WP_Query();
  322.             $count_query->query( $query_args );
  323.             $total_posts = $count_query->found_posts;
  324.         }
  325.  
  326.         $max_pages = ceil( $total_posts / (int) $posts_query->query_vars['posts_per_page'] );
  327.  
  328.         if ( $page > $max_pages && $total_posts > 0 ) {
  329.             return new WP_Error( 'rest_post_invalid_page_number', __( 'The page number requested is larger than the number of pages available.' ), array( 'status' => 400 ) );
  330.         }
  331.  
  332.         $response  = rest_ensure_response( $posts );
  333.  
  334.         $response->header( 'X-WP-Total', (int) $total_posts );
  335.         $response->header( 'X-WP-TotalPages', (int) $max_pages );
  336.  
  337.         $request_params = $request->get_query_params();
  338.         $base = add_query_arg( $request_params, rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
  339.  
  340.         if ( $page > 1 ) {
  341.             $prev_page = $page - 1;
  342.  
  343.             if ( $prev_page > $max_pages ) {
  344.                 $prev_page = $max_pages;
  345.             }
  346.  
  347.             $prev_link = add_query_arg( 'page', $prev_page, $base );
  348.             $response->link_header( 'prev', $prev_link );
  349.         }
  350.         if ( $max_pages > $page ) {
  351.             $next_page = $page + 1;
  352.             $next_link = add_query_arg( 'page', $next_page, $base );
  353.  
  354.             $response->link_header( 'next', $next_link );
  355.         }
  356.  
  357.         return $response;
  358.     }
  359.  
  360.     /**
  361.      * Get the post, if the ID is valid.
  362.      *
  363.      * @since 4.7.2
  364.      *
  365.      * @param int $id Supplied ID.
  366.      * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
  367.      */
  368.     protected function get_post( $id ) {
  369.         $error = new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
  370.         if ( (int) $id <= 0 ) {
  371.             return $error;
  372.         }
  373.  
  374.         $post = get_post( (int) $id );
  375.         if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
  376.             return $error;
  377.         }
  378.  
  379.         return $post;
  380.     }
  381.  
  382.     /**
  383.      * Checks if a given request has access to read a post.
  384.      *
  385.      * @since 4.7.0
  386.      *
  387.      * @param WP_REST_Request $request Full details about the request.
  388.      * @return bool|WP_Error True if the request has read access for the item, WP_Error object otherwise.
  389.      */
  390.     public function get_item_permissions_check( $request ) {
  391.         $post = $this->get_post( $request['id'] );
  392.         if ( is_wp_error( $post ) ) {
  393.             return $post;
  394.         }
  395.  
  396.         if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
  397.             return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => rest_authorization_required_code() ) );
  398.         }
  399.  
  400.         if ( $post && ! empty( $request['password'] ) ) {
  401.             // Check post password, and return error if invalid.
  402.             if ( ! hash_equals( $post->post_password, $request['password'] ) ) {
  403.                 return new WP_Error( 'rest_post_incorrect_password', __( 'Incorrect post password.' ), array( 'status' => 403 ) );
  404.             }
  405.         }
  406.  
  407.         // Allow access to all password protected posts if the context is edit.
  408.         if ( 'edit' === $request['context'] ) {
  409.             add_filter( 'post_password_required', '__return_false' );
  410.         }
  411.  
  412.         if ( $post ) {
  413.             return $this->check_read_permission( $post );
  414.         }
  415.  
  416.         return true;
  417.     }
  418.  
  419.     /**
  420.      * Checks if the user can access password-protected content.
  421.      *
  422.      * This method determines whether we need to override the regular password
  423.      * check in core with a filter.
  424.      *
  425.      * @since 4.7.0
  426.      *
  427.      * @param WP_Post         $post    Post to check against.
  428.      * @param WP_REST_Request $request Request data to check.
  429.      * @return bool True if the user can access password-protected content, otherwise false.
  430.      */
  431.     public function can_access_password_content( $post, $request ) {
  432.         if ( empty( $post->post_password ) ) {
  433.             // No filter required.
  434.             return false;
  435.         }
  436.  
  437.         // Edit context always gets access to password-protected posts.
  438.         if ( 'edit' === $request['context'] ) {
  439.             return true;
  440.         }
  441.  
  442.         // No password, no auth.
  443.         if ( empty( $request['password'] ) ) {
  444.             return false;
  445.         }
  446.  
  447.         // Double-check the request password.
  448.         return hash_equals( $post->post_password, $request['password'] );
  449.     }
  450.  
  451.     /**
  452.      * Retrieves a single post.
  453.      *
  454.      * @since 4.7.0
  455.      *
  456.      * @param WP_REST_Request $request Full details about the request.
  457.      * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  458.      */
  459.     public function get_item( $request ) {
  460.         $post = $this->get_post( $request['id'] );
  461.         if ( is_wp_error( $post ) ) {
  462.             return $post;
  463.         }
  464.  
  465.         $data     = $this->prepare_item_for_response( $post, $request );
  466.         $response = rest_ensure_response( $data );
  467.  
  468.         if ( is_post_type_viewable( get_post_type_object( $post->post_type ) ) ) {
  469.             $response->link_header( 'alternate',  get_permalink( $post->ID ), array( 'type' => 'text/html' ) );
  470.         }
  471.  
  472.         return $response;
  473.     }
  474.  
  475.     /**
  476.      * Checks if a given request has access to create a post.
  477.      *
  478.      * @since 4.7.0
  479.      *
  480.      * @param WP_REST_Request $request Full details about the request.
  481.      * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
  482.      */
  483.     public function create_item_permissions_check( $request ) {
  484.         if ( ! empty( $request['id'] ) ) {
  485.             return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.' ), array( 'status' => 400 ) );
  486.         }
  487.  
  488.         $post_type = get_post_type_object( $this->post_type );
  489.  
  490.         if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
  491.             return new WP_Error( 'rest_cannot_edit_others', __( 'Sorry, you are not allowed to create posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
  492.         }
  493.  
  494.         if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
  495.             return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
  496.         }
  497.  
  498.         if ( ! current_user_can( $post_type->cap->create_posts ) ) {
  499.             return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to create posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
  500.         }
  501.  
  502.         if ( ! $this->check_assign_terms_permission( $request ) ) {
  503.             return new WP_Error( 'rest_cannot_assign_term', __( 'Sorry, you are not allowed to assign the provided terms.' ), array( 'status' => rest_authorization_required_code() ) );
  504.         }
  505.  
  506.         return true;
  507.     }
  508.  
  509.     /**
  510.      * Creates a single post.
  511.      *
  512.      * @since 4.7.0
  513.      *
  514.      * @param WP_REST_Request $request Full details about the request.
  515.      * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  516.      */
  517.     public function create_item( $request ) {
  518.         if ( ! empty( $request['id'] ) ) {
  519.             return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.' ), array( 'status' => 400 ) );
  520.         }
  521.  
  522.         $prepared_post = $this->prepare_item_for_database( $request );
  523.  
  524.         if ( is_wp_error( $prepared_post ) ) {
  525.             return $prepared_post;
  526.         }
  527.  
  528.         $prepared_post->post_type = $this->post_type;
  529.  
  530.         $post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true );
  531.  
  532.         if ( is_wp_error( $post_id ) ) {
  533.  
  534.             if ( 'db_insert_error' === $post_id->get_error_code() ) {
  535.                 $post_id->add_data( array( 'status' => 500 ) );
  536.             } else {
  537.                 $post_id->add_data( array( 'status' => 400 ) );
  538.             }
  539.  
  540.             return $post_id;
  541.         }
  542.  
  543.         $post = get_post( $post_id );
  544.  
  545.         /**
  546.          * Fires after a single post is created or updated via the REST API.
  547.          *
  548.          * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
  549.          *
  550.          * @since 4.7.0
  551.          *
  552.          * @param WP_Post         $post     Inserted or updated post object.
  553.          * @param WP_REST_Request $request  Request object.
  554.          * @param bool            $creating True when creating a post, false when updating.
  555.          */
  556.         do_action( "rest_insert_{$this->post_type}", $post, $request, true );
  557.  
  558.         $schema = $this->get_item_schema();
  559.  
  560.         if ( ! empty( $schema['properties']['sticky'] ) ) {
  561.             if ( ! empty( $request['sticky'] ) ) {
  562.                 stick_post( $post_id );
  563.             } else {
  564.                 unstick_post( $post_id );
  565.             }
  566.         }
  567.  
  568.         if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
  569.             $this->handle_featured_media( $request['featured_media'], $post_id );
  570.         }
  571.  
  572.         if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
  573.             set_post_format( $post, $request['format'] );
  574.         }
  575.  
  576.         if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
  577.             $this->handle_template( $request['template'], $post_id, true );
  578.         }
  579.  
  580.         $terms_update = $this->handle_terms( $post_id, $request );
  581.  
  582.         if ( is_wp_error( $terms_update ) ) {
  583.             return $terms_update;
  584.         }
  585.  
  586.         if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
  587.             $meta_update = $this->meta->update_value( $request['meta'], $post_id );
  588.  
  589.             if ( is_wp_error( $meta_update ) ) {
  590.                 return $meta_update;
  591.             }
  592.         }
  593.  
  594.         $post = get_post( $post_id );
  595.         $fields_update = $this->update_additional_fields_for_object( $post, $request );
  596.  
  597.         if ( is_wp_error( $fields_update ) ) {
  598.             return $fields_update;
  599.         }
  600.  
  601.         $request->set_param( 'context', 'edit' );
  602.  
  603.         $response = $this->prepare_item_for_response( $post, $request );
  604.         $response = rest_ensure_response( $response );
  605.  
  606.         $response->set_status( 201 );
  607.         $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) );
  608.  
  609.         return $response;
  610.     }
  611.  
  612.     /**
  613.      * Checks if a given request has access to update a post.
  614.      *
  615.      * @since 4.7.0
  616.      *
  617.      * @param WP_REST_Request $request Full details about the request.
  618.      * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
  619.      */
  620.     public function update_item_permissions_check( $request ) {
  621.         $post = $this->get_post( $request['id'] );
  622.         if ( is_wp_error( $post ) ) {
  623.             return $post;
  624.         }
  625.  
  626.         $post_type = get_post_type_object( $this->post_type );
  627.  
  628.         if ( $post && ! $this->check_update_permission( $post ) ) {
  629.             return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => rest_authorization_required_code() ) );
  630.         }
  631.  
  632.         if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
  633.             return new WP_Error( 'rest_cannot_edit_others', __( 'Sorry, you are not allowed to update posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
  634.         }
  635.  
  636.         if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
  637.             return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
  638.         }
  639.  
  640.         if ( ! $this->check_assign_terms_permission( $request ) ) {
  641.             return new WP_Error( 'rest_cannot_assign_term', __( 'Sorry, you are not allowed to assign the provided terms.' ), array( 'status' => rest_authorization_required_code() ) );
  642.         }
  643.  
  644.         return true;
  645.     }
  646.  
  647.     /**
  648.      * Updates a single post.
  649.      *
  650.      * @since 4.7.0
  651.      *
  652.      * @param WP_REST_Request $request Full details about the request.
  653.      * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  654.      */
  655.     public function update_item( $request ) {
  656.         $valid_check = $this->get_post( $request['id'] );
  657.         if ( is_wp_error( $valid_check ) ) {
  658.             return $valid_check;
  659.         }
  660.  
  661.         $post = $this->prepare_item_for_database( $request );
  662.  
  663.         if ( is_wp_error( $post ) ) {
  664.             return $post;
  665.         }
  666.  
  667.         // convert the post object to an array, otherwise wp_update_post will expect non-escaped input.
  668.         $post_id = wp_update_post( wp_slash( (array) $post ), true );
  669.  
  670.         if ( is_wp_error( $post_id ) ) {
  671.             if ( 'db_update_error' === $post_id->get_error_code() ) {
  672.                 $post_id->add_data( array( 'status' => 500 ) );
  673.             } else {
  674.                 $post_id->add_data( array( 'status' => 400 ) );
  675.             }
  676.             return $post_id;
  677.         }
  678.  
  679.         $post = get_post( $post_id );
  680.  
  681.         /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
  682.         do_action( "rest_insert_{$this->post_type}", $post, $request, false );
  683.  
  684.         $schema = $this->get_item_schema();
  685.  
  686.         if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
  687.             set_post_format( $post, $request['format'] );
  688.         }
  689.  
  690.         if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
  691.             $this->handle_featured_media( $request['featured_media'], $post_id );
  692.         }
  693.  
  694.         if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) {
  695.             if ( ! empty( $request['sticky'] ) ) {
  696.                 stick_post( $post_id );
  697.             } else {
  698.                 unstick_post( $post_id );
  699.             }
  700.         }
  701.  
  702.         if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
  703.             $this->handle_template( $request['template'], $post->ID );
  704.         }
  705.  
  706.         $terms_update = $this->handle_terms( $post->ID, $request );
  707.  
  708.         if ( is_wp_error( $terms_update ) ) {
  709.             return $terms_update;
  710.         }
  711.  
  712.         if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
  713.             $meta_update = $this->meta->update_value( $request['meta'], $post->ID );
  714.  
  715.             if ( is_wp_error( $meta_update ) ) {
  716.                 return $meta_update;
  717.             }
  718.         }
  719.  
  720.         $post = get_post( $post_id );
  721.         $fields_update = $this->update_additional_fields_for_object( $post, $request );
  722.  
  723.         if ( is_wp_error( $fields_update ) ) {
  724.             return $fields_update;
  725.         }
  726.  
  727.         $request->set_param( 'context', 'edit' );
  728.  
  729.         $response = $this->prepare_item_for_response( $post, $request );
  730.  
  731.         return rest_ensure_response( $response );
  732.     }
  733.  
  734.     /**
  735.      * Checks if a given request has access to delete a post.
  736.      *
  737.      * @since 4.7.0
  738.      *
  739.      * @param WP_REST_Request $request Full details about the request.
  740.      * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
  741.      */
  742.     public function delete_item_permissions_check( $request ) {
  743.         $post = $this->get_post( $request['id'] );
  744.         if ( is_wp_error( $post ) ) {
  745.             return $post;
  746.         }
  747.  
  748.         if ( $post && ! $this->check_delete_permission( $post ) ) {
  749.             return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => rest_authorization_required_code() ) );
  750.         }
  751.  
  752.         return true;
  753.     }
  754.  
  755.     /**
  756.      * Deletes a single post.
  757.      *
  758.      * @since 4.7.0
  759.      *
  760.      * @param WP_REST_Request $request Full details about the request.
  761.      * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  762.      */
  763.     public function delete_item( $request ) {
  764.         $post = $this->get_post( $request['id'] );
  765.         if ( is_wp_error( $post ) ) {
  766.             return $post;
  767.         }
  768.  
  769.         $id    = $post->ID;
  770.         $force = (bool) $request['force'];
  771.  
  772.         $supports_trash = ( EMPTY_TRASH_DAYS > 0 );
  773.  
  774.         if ( 'attachment' === $post->post_type ) {
  775.             $supports_trash = $supports_trash && MEDIA_TRASH;
  776.         }
  777.  
  778.         /**
  779.          * Filters whether a post is trashable.
  780.          *
  781.          * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
  782.          *
  783.          * Pass false to disable trash support for the post.
  784.          *
  785.          * @since 4.7.0
  786.          *
  787.          * @param bool    $supports_trash Whether the post type support trashing.
  788.          * @param WP_Post $post           The Post object being considered for trashing support.
  789.          */
  790.         $supports_trash = apply_filters( "rest_{$this->post_type}_trashable", $supports_trash, $post );
  791.  
  792.         if ( ! $this->check_delete_permission( $post ) ) {
  793.             return new WP_Error( 'rest_user_cannot_delete_post', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => rest_authorization_required_code() ) );
  794.         }
  795.  
  796.         $request->set_param( 'context', 'edit' );
  797.  
  798.  
  799.         // If we're forcing, then delete permanently.
  800.         if ( $force ) {
  801.             $previous = $this->prepare_item_for_response( $post, $request );
  802.             $result = wp_delete_post( $id, true );
  803.             $response = new WP_REST_Response();
  804.             $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data() ) );
  805.         } else {
  806.             // If we don't support trashing for this type, error out.
  807.             if ( ! $supports_trash ) {
  808.                 /* translators: %s: force=true */
  809.                 return new WP_Error( 'rest_trash_not_supported', sprintf( __( "The post does not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) );
  810.             }
  811.  
  812.             // Otherwise, only trash if we haven't already.
  813.             if ( 'trash' === $post->post_status ) {
  814.                 return new WP_Error( 'rest_already_trashed', __( 'The post has already been deleted.' ), array( 'status' => 410 ) );
  815.             }
  816.  
  817.             // (Note that internally this falls through to `wp_delete_post` if
  818.             // the trash is disabled.)
  819.             $result = wp_trash_post( $id );
  820.             $post = get_post( $id );
  821.             $response = $this->prepare_item_for_response( $post, $request );
  822.         }
  823.  
  824.         if ( ! $result ) {
  825.             return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
  826.         }
  827.  
  828.         /**
  829.          * Fires immediately after a single post is deleted or trashed via the REST API.
  830.          *
  831.          * They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
  832.          *
  833.          * @since 4.7.0
  834.          *
  835.          * @param object           $post     The deleted or trashed post.
  836.          * @param WP_REST_Response $response The response data.
  837.          * @param WP_REST_Request  $request  The request sent to the API.
  838.          */
  839.         do_action( "rest_delete_{$this->post_type}", $post, $response, $request );
  840.  
  841.         return $response;
  842.     }
  843.  
  844.     /**
  845.      * Determines the allowed query_vars for a get_items() response and prepares
  846.      * them for WP_Query.
  847.      *
  848.      * @since 4.7.0
  849.      *
  850.      * @param array           $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
  851.      * @param WP_REST_Request $request       Optional. Full details about the request.
  852.      * @return array Items query arguments.
  853.      */
  854.     protected function prepare_items_query( $prepared_args = array(), $request = null ) {
  855.         $query_args = array();
  856.  
  857.         foreach ( $prepared_args as $key => $value ) {
  858.             /**
  859.              * Filters the query_vars used in get_items() for the constructed query.
  860.              *
  861.              * The dynamic portion of the hook name, `$key`, refers to the query_var key.
  862.              *
  863.              * @since 4.7.0
  864.              *
  865.              * @param string $value The query_var value.
  866.              */
  867.             $query_args[ $key ] = apply_filters( "rest_query_var-{$key}", $value );
  868.         }
  869.  
  870.         if ( 'post' !== $this->post_type || ! isset( $query_args['ignore_sticky_posts'] ) ) {
  871.             $query_args['ignore_sticky_posts'] = true;
  872.         }
  873.  
  874.         // Map to proper WP_Query orderby param.
  875.         if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
  876.             $orderby_mappings = array(
  877.                 'id'            => 'ID',
  878.                 'include'       => 'post__in',
  879.                 'slug'          => 'post_name',
  880.                 'include_slugs' => 'post_name__in',
  881.             );
  882.  
  883.             if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
  884.                 $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
  885.             }
  886.         }
  887.  
  888.         return $query_args;
  889.     }
  890.  
  891.     /**
  892.      * Checks the post_date_gmt or modified_gmt and prepare any post or
  893.      * modified date for single post output.
  894.      *
  895.      * @since 4.7.0
  896.      *
  897.      * @param string      $date_gmt GMT publication time.
  898.      * @param string|null $date     Optional. Local publication time. Default null.
  899.      * @return string|null ISO8601/RFC3339 formatted datetime.
  900.      */
  901.     protected function prepare_date_response( $date_gmt, $date = null ) {
  902.         // Use the date if passed.
  903.         if ( isset( $date ) ) {
  904.             return mysql_to_rfc3339( $date );
  905.         }
  906.  
  907.         // Return null if $date_gmt is empty/zeros.
  908.         if ( '0000-00-00 00:00:00' === $date_gmt ) {
  909.             return null;
  910.         }
  911.  
  912.         // Return the formatted datetime.
  913.         return mysql_to_rfc3339( $date_gmt );
  914.     }
  915.  
  916.     /**
  917.      * Prepares a single post for create or update.
  918.      *
  919.      * @since 4.7.0
  920.      *
  921.      * @param WP_REST_Request $request Request object.
  922.      * @return stdClass|WP_Error Post object or WP_Error.
  923.      */
  924.     protected function prepare_item_for_database( $request ) {
  925.         $prepared_post = new stdClass;
  926.  
  927.         // Post ID.
  928.         if ( isset( $request['id'] ) ) {
  929.             $existing_post = $this->get_post( $request['id'] );
  930.             if ( is_wp_error( $existing_post ) ) {
  931.                 return $existing_post;
  932.             }
  933.  
  934.             $prepared_post->ID = $existing_post->ID;
  935.         }
  936.  
  937.         $schema = $this->get_item_schema();
  938.  
  939.         // Post title.
  940.         if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
  941.             if ( is_string( $request['title'] ) ) {
  942.                 $prepared_post->post_title = $request['title'];
  943.             } elseif ( ! empty( $request['title']['raw'] ) ) {
  944.                 $prepared_post->post_title = $request['title']['raw'];
  945.             }
  946.         }
  947.  
  948.         // Post content.
  949.         if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
  950.             if ( is_string( $request['content'] ) ) {
  951.                 $prepared_post->post_content = $request['content'];
  952.             } elseif ( isset( $request['content']['raw'] ) ) {
  953.                 $prepared_post->post_content = $request['content']['raw'];
  954.             }
  955.         }
  956.  
  957.         // Post excerpt.
  958.         if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) {
  959.             if ( is_string( $request['excerpt'] ) ) {
  960.                 $prepared_post->post_excerpt = $request['excerpt'];
  961.             } elseif ( isset( $request['excerpt']['raw'] ) ) {
  962.                 $prepared_post->post_excerpt = $request['excerpt']['raw'];
  963.             }
  964.         }
  965.  
  966.         // Post type.
  967.         if ( empty( $request['id'] ) ) {
  968.             // Creating new post, use default type for the controller.
  969.             $prepared_post->post_type = $this->post_type;
  970.         } else {
  971.             // Updating a post, use previous type.
  972.             $prepared_post->post_type = get_post_type( $request['id'] );
  973.         }
  974.  
  975.         $post_type = get_post_type_object( $prepared_post->post_type );
  976.  
  977.         // Post status.
  978.         if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) ) {
  979.             $status = $this->handle_status_param( $request['status'], $post_type );
  980.  
  981.             if ( is_wp_error( $status ) ) {
  982.                 return $status;
  983.             }
  984.  
  985.             $prepared_post->post_status = $status;
  986.         }
  987.  
  988.         // Post date.
  989.         if ( ! empty( $schema['properties']['date'] ) && ! empty( $request['date'] ) ) {
  990.             $date_data = rest_get_date_with_gmt( $request['date'] );
  991.  
  992.             if ( ! empty( $date_data ) ) {
  993.                 list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
  994.                 $prepared_post->edit_date = true;
  995.             }
  996.         } elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) {
  997.             $date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
  998.  
  999.             if ( ! empty( $date_data ) ) {
  1000.                 list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
  1001.                 $prepared_post->edit_date = true;
  1002.             }
  1003.         }
  1004.  
  1005.         // Post slug.
  1006.         if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) {
  1007.             $prepared_post->post_name = $request['slug'];
  1008.         }
  1009.  
  1010.         // Author.
  1011.         if ( ! empty( $schema['properties']['author'] ) && ! empty( $request['author'] ) ) {
  1012.             $post_author = (int) $request['author'];
  1013.  
  1014.             if ( get_current_user_id() !== $post_author ) {
  1015.                 $user_obj = get_userdata( $post_author );
  1016.  
  1017.                 if ( ! $user_obj ) {
  1018.                     return new WP_Error( 'rest_invalid_author', __( 'Invalid author ID.' ), array( 'status' => 400 ) );
  1019.                 }
  1020.             }
  1021.  
  1022.             $prepared_post->post_author = $post_author;
  1023.         }
  1024.  
  1025.         // Post password.
  1026.         if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) ) {
  1027.             $prepared_post->post_password = $request['password'];
  1028.  
  1029.             if ( '' !== $request['password'] ) {
  1030.                 if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
  1031.                     return new WP_Error( 'rest_invalid_field', __( 'A post can not be sticky and have a password.' ), array( 'status' => 400 ) );
  1032.                 }
  1033.  
  1034.                 if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) {
  1035.                     return new WP_Error( 'rest_invalid_field', __( 'A sticky post can not be password protected.' ), array( 'status' => 400 ) );
  1036.                 }
  1037.             }
  1038.         }
  1039.  
  1040.         if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
  1041.             if ( ! empty( $prepared_post->ID ) && post_password_required( $prepared_post->ID ) ) {
  1042.                 return new WP_Error( 'rest_invalid_field', __( 'A password protected post can not be set to sticky.' ), array( 'status' => 400 ) );
  1043.             }
  1044.         }
  1045.  
  1046.         // Parent.
  1047.         if ( ! empty( $schema['properties']['parent'] ) && isset( $request['parent'] ) ) {
  1048.             if ( 0 === (int) $request['parent'] ) {
  1049.                 $prepared_post->post_parent = 0;
  1050.             } else {
  1051.                 $parent = get_post( (int) $request['parent'] );
  1052.                 if ( empty( $parent ) ) {
  1053.                     return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post parent ID.' ), array( 'status' => 400 ) );
  1054.                 }
  1055.                 $prepared_post->post_parent = (int) $parent->ID;
  1056.             }
  1057.         }
  1058.  
  1059.         // Menu order.
  1060.         if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) {
  1061.             $prepared_post->menu_order = (int) $request['menu_order'];
  1062.         }
  1063.  
  1064.         // Comment status.
  1065.         if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) {
  1066.             $prepared_post->comment_status = $request['comment_status'];
  1067.         }
  1068.  
  1069.         // Ping status.
  1070.         if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) {
  1071.             $prepared_post->ping_status = $request['ping_status'];
  1072.         }
  1073.  
  1074.         if ( ! empty( $schema['properties']['template'] ) ) {
  1075.             // Force template to null so that it can be handled exclusively by the REST controller.
  1076.             $prepared_post->page_template = null;
  1077.         }
  1078.  
  1079.         /**
  1080.          * Filters a post before it is inserted via the REST API.
  1081.          *
  1082.          * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
  1083.          *
  1084.          * @since 4.7.0
  1085.          *
  1086.          * @param stdClass        $prepared_post An object representing a single post prepared
  1087.          *                                       for inserting or updating the database.
  1088.          * @param WP_REST_Request $request       Request object.
  1089.          */
  1090.         return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
  1091.  
  1092.     }
  1093.  
  1094.     /**
  1095.      * Determines validity and normalizes the given status parameter.
  1096.      *
  1097.      * @since 4.7.0
  1098.      *
  1099.      * @param string $post_status Post status.
  1100.      * @param object $post_type   Post type.
  1101.      * @return string|WP_Error Post status or WP_Error if lacking the proper permission.
  1102.      */
  1103.     protected function handle_status_param( $post_status, $post_type ) {
  1104.  
  1105.         switch ( $post_status ) {
  1106.             case 'draft':
  1107.             case 'pending':
  1108.                 break;
  1109.             case 'private':
  1110.                 if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
  1111.                     return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to create private posts in this post type.' ), array( 'status' => rest_authorization_required_code() ) );
  1112.                 }
  1113.                 break;
  1114.             case 'publish':
  1115.             case 'future':
  1116.                 if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
  1117.                     return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to publish posts in this post type.' ), array( 'status' => rest_authorization_required_code() ) );
  1118.                 }
  1119.                 break;
  1120.             default:
  1121.                 if ( ! get_post_status_object( $post_status ) ) {
  1122.                     $post_status = 'draft';
  1123.                 }
  1124.                 break;
  1125.         }
  1126.  
  1127.         return $post_status;
  1128.     }
  1129.  
  1130.     /**
  1131.      * Determines the featured media based on a request param.
  1132.      *
  1133.      * @since 4.7.0
  1134.      *
  1135.      * @param int $featured_media Featured Media ID.
  1136.      * @param int $post_id        Post ID.
  1137.      * @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error.
  1138.      */
  1139.     protected function handle_featured_media( $featured_media, $post_id ) {
  1140.  
  1141.         $featured_media = (int) $featured_media;
  1142.         if ( $featured_media ) {
  1143.             $result = set_post_thumbnail( $post_id, $featured_media );
  1144.             if ( $result ) {
  1145.                 return true;
  1146.             } else {
  1147.                 return new WP_Error( 'rest_invalid_featured_media', __( 'Invalid featured media ID.' ), array( 'status' => 400 ) );
  1148.             }
  1149.         } else {
  1150.             return delete_post_thumbnail( $post_id );
  1151.         }
  1152.  
  1153.     }
  1154.  
  1155.     /**
  1156.      * Check whether the template is valid for the given post.
  1157.      *
  1158.      * @since 4.9.0
  1159.      *
  1160.      * @param string          $template Page template filename.
  1161.      * @param WP_REST_Request $request  Request.
  1162.      * @return bool|WP_Error True if template is still valid or if the same as existing value, or false if template not supported.
  1163.      */
  1164.     public function check_template( $template, $request ) {
  1165.  
  1166.         if ( ! $template ) {
  1167.             return true;
  1168.         }
  1169.  
  1170.         if ( $request['id'] ) {
  1171.             $current_template = get_page_template_slug( $request['id'] );
  1172.         } else {
  1173.             $current_template = '';
  1174.         }
  1175.  
  1176.         // Always allow for updating a post to the same template, even if that template is no longer supported.
  1177.         if ( $template === $current_template ) {
  1178.             return true;
  1179.         }
  1180.  
  1181.         // If this is a create request, get_post() will return null and wp theme will fallback to the passed post type.
  1182.         $allowed_templates = wp_get_theme()->get_page_templates( get_post( $request['id'] ), $this->post_type );
  1183.  
  1184.         if ( isset( $allowed_templates[ $template ] ) ) {
  1185.             return true;
  1186.         }
  1187.  
  1188.         /* translators: 1: parameter, 2: list of valid values */
  1189.         return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not one of %2$s.' ), 'template', implode( ', ', array_keys( $allowed_templates ) ) ) );
  1190.     }
  1191.  
  1192.     /**
  1193.      * Sets the template for a post.
  1194.      *
  1195.      * @since 4.7.0
  1196.      * @since 4.9.0 Introduced the $validate parameter.
  1197.      *
  1198.      * @param string  $template Page template filename.
  1199.      * @param integer $post_id  Post ID.
  1200.      * @param bool    $validate Whether to validate that the template selected is valid.
  1201.      */
  1202.     public function handle_template( $template, $post_id, $validate = false ) {
  1203.  
  1204.         if ( $validate && ! array_key_exists( $template, wp_get_theme()->get_page_templates( get_post( $post_id ) ) ) ) {
  1205.             $template = '';
  1206.         }
  1207.  
  1208.         update_post_meta( $post_id, '_wp_page_template', $template );
  1209.     }
  1210.  
  1211.     /**
  1212.      * Updates the post's terms from a REST request.
  1213.      *
  1214.      * @since 4.7.0
  1215.      *
  1216.      * @param int             $post_id The post ID to update the terms form.
  1217.      * @param WP_REST_Request $request The request object with post and terms data.
  1218.      * @return null|WP_Error WP_Error on an error assigning any of the terms, otherwise null.
  1219.      */
  1220.     protected function handle_terms( $post_id, $request ) {
  1221.         $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
  1222.  
  1223.         foreach ( $taxonomies as $taxonomy ) {
  1224.             $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
  1225.  
  1226.             if ( ! isset( $request[ $base ] ) ) {
  1227.                 continue;
  1228.             }
  1229.  
  1230.             $result = wp_set_object_terms( $post_id, $request[ $base ], $taxonomy->name );
  1231.  
  1232.             if ( is_wp_error( $result ) ) {
  1233.                 return $result;
  1234.             }
  1235.         }
  1236.     }
  1237.  
  1238.     /**
  1239.      * Checks whether current user can assign all terms sent with the current request.
  1240.      *
  1241.      * @since 4.7.0
  1242.      *
  1243.      * @param WP_REST_Request $request The request object with post and terms data.
  1244.      * @return bool Whether the current user can assign the provided terms.
  1245.      */
  1246.     protected function check_assign_terms_permission( $request ) {
  1247.         $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
  1248.         foreach ( $taxonomies as $taxonomy ) {
  1249.             $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
  1250.  
  1251.             if ( ! isset( $request[ $base ] ) ) {
  1252.                 continue;
  1253.             }
  1254.  
  1255.             foreach ( $request[ $base ] as $term_id ) {
  1256.                 // Invalid terms will be rejected later.
  1257.                 if ( ! get_term( $term_id, $taxonomy->name ) ) {
  1258.                     continue;
  1259.                 }
  1260.  
  1261.                 if ( ! current_user_can( 'assign_term', (int) $term_id ) ) {
  1262.                     return false;
  1263.                 }
  1264.             }
  1265.         }
  1266.  
  1267.         return true;
  1268.     }
  1269.  
  1270.     /**
  1271.      * Checks if a given post type can be viewed or managed.
  1272.      *
  1273.      * @since 4.7.0
  1274.      *
  1275.      * @param object|string $post_type Post type name or object.
  1276.      * @return bool Whether the post type is allowed in REST.
  1277.      */
  1278.     protected function check_is_post_type_allowed( $post_type ) {
  1279.         if ( ! is_object( $post_type ) ) {
  1280.             $post_type = get_post_type_object( $post_type );
  1281.         }
  1282.  
  1283.         if ( ! empty( $post_type ) && ! empty( $post_type->show_in_rest ) ) {
  1284.             return true;
  1285.         }
  1286.  
  1287.         return false;
  1288.     }
  1289.  
  1290.     /**
  1291.      * Checks if a post can be read.
  1292.      *
  1293.      * Correctly handles posts with the inherit status.
  1294.      *
  1295.      * @since 4.7.0
  1296.      *
  1297.      * @param object $post Post object.
  1298.      * @return bool Whether the post can be read.
  1299.      */
  1300.     public function check_read_permission( $post ) {
  1301.         $post_type = get_post_type_object( $post->post_type );
  1302.         if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
  1303.             return false;
  1304.         }
  1305.  
  1306.         // Is the post readable?
  1307.         if ( 'publish' === $post->post_status || current_user_can( $post_type->cap->read_post, $post->ID ) ) {
  1308.             return true;
  1309.         }
  1310.  
  1311.         $post_status_obj = get_post_status_object( $post->post_status );
  1312.         if ( $post_status_obj && $post_status_obj->public ) {
  1313.             return true;
  1314.         }
  1315.  
  1316.         // Can we read the parent if we're inheriting?
  1317.         if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
  1318.             $parent = get_post( $post->post_parent );
  1319.             if ( $parent ) {
  1320.                 return $this->check_read_permission( $parent );
  1321.             }
  1322.         }
  1323.  
  1324.         /*
  1325.          * If there isn't a parent, but the status is set to inherit, assume
  1326.          * it's published (as per get_post_status()).
  1327.          */
  1328.         if ( 'inherit' === $post->post_status ) {
  1329.             return true;
  1330.         }
  1331.  
  1332.         return false;
  1333.     }
  1334.  
  1335.     /**
  1336.      * Checks if a post can be edited.
  1337.      *
  1338.      * @since 4.7.0
  1339.      *
  1340.      * @param object $post Post object.
  1341.      * @return bool Whether the post can be edited.
  1342.      */
  1343.     protected function check_update_permission( $post ) {
  1344.         $post_type = get_post_type_object( $post->post_type );
  1345.  
  1346.         if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
  1347.             return false;
  1348.         }
  1349.  
  1350.         return current_user_can( $post_type->cap->edit_post, $post->ID );
  1351.     }
  1352.  
  1353.     /**
  1354.      * Checks if a post can be created.
  1355.      *
  1356.      * @since 4.7.0
  1357.      *
  1358.      * @param object $post Post object.
  1359.      * @return bool Whether the post can be created.
  1360.      */
  1361.     protected function check_create_permission( $post ) {
  1362.         $post_type = get_post_type_object( $post->post_type );
  1363.  
  1364.         if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
  1365.             return false;
  1366.         }
  1367.  
  1368.         return current_user_can( $post_type->cap->create_posts );
  1369.     }
  1370.  
  1371.     /**
  1372.      * Checks if a post can be deleted.
  1373.      *
  1374.      * @since 4.7.0
  1375.      *
  1376.      * @param object $post Post object.
  1377.      * @return bool Whether the post can be deleted.
  1378.      */
  1379.     protected function check_delete_permission( $post ) {
  1380.         $post_type = get_post_type_object( $post->post_type );
  1381.  
  1382.         if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
  1383.             return false;
  1384.         }
  1385.  
  1386.         return current_user_can( $post_type->cap->delete_post, $post->ID );
  1387.     }
  1388.  
  1389.     /**
  1390.      * Prepares a single post output for response.
  1391.      *
  1392.      * @since 4.7.0
  1393.      *
  1394.      * @param WP_Post         $post    Post object.
  1395.      * @param WP_REST_Request $request Request object.
  1396.      * @return WP_REST_Response Response object.
  1397.      */
  1398.     public function prepare_item_for_response( $post, $request ) {
  1399.         $GLOBALS['post'] = $post;
  1400.  
  1401.         setup_postdata( $post );
  1402.  
  1403.         $schema = $this->get_item_schema();
  1404.  
  1405.         // Base fields for every post.
  1406.         $data = array();
  1407.  
  1408.         if ( ! empty( $schema['properties']['id'] ) ) {
  1409.             $data['id'] = $post->ID;
  1410.         }
  1411.  
  1412.         if ( ! empty( $schema['properties']['date'] ) ) {
  1413.             $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
  1414.         }
  1415.  
  1416.         if ( ! empty( $schema['properties']['date_gmt'] ) ) {
  1417.             // For drafts, `post_date_gmt` may not be set, indicating that the
  1418.             // date of the draft should be updated each time it is saved (see
  1419.             // #38883).  In this case, shim the value based on the `post_date`
  1420.             // field with the site's timezone offset applied.
  1421.             if ( '0000-00-00 00:00:00' === $post->post_date_gmt ) {
  1422.                 $post_date_gmt = get_gmt_from_date( $post->post_date );
  1423.             } else {
  1424.                 $post_date_gmt = $post->post_date_gmt;
  1425.             }
  1426.             $data['date_gmt'] = $this->prepare_date_response( $post_date_gmt );
  1427.         }
  1428.  
  1429.         if ( ! empty( $schema['properties']['guid'] ) ) {
  1430.             $data['guid'] = array(
  1431.                 /** This filter is documented in wp-includes/post-template.php */
  1432.                 'rendered' => apply_filters( 'get_the_guid', $post->guid, $post->ID ),
  1433.                 'raw'      => $post->guid,
  1434.             );
  1435.         }
  1436.  
  1437.         if ( ! empty( $schema['properties']['modified'] ) ) {
  1438.             $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
  1439.         }
  1440.  
  1441.         if ( ! empty( $schema['properties']['modified_gmt'] ) ) {
  1442.             // For drafts, `post_modified_gmt` may not be set (see
  1443.             // `post_date_gmt` comments above).  In this case, shim the value
  1444.             // based on the `post_modified` field with the site's timezone
  1445.             // offset applied.
  1446.             if ( '0000-00-00 00:00:00' === $post->post_modified_gmt ) {
  1447.                 $post_modified_gmt = date( 'Y-m-d H:i:s', strtotime( $post->post_modified ) - ( get_option( 'gmt_offset' ) * 3600 ) );
  1448.             } else {
  1449.                 $post_modified_gmt = $post->post_modified_gmt;
  1450.             }
  1451.             $data['modified_gmt'] = $this->prepare_date_response( $post_modified_gmt );
  1452.         }
  1453.  
  1454.         if ( ! empty( $schema['properties']['password'] ) ) {
  1455.             $data['password'] = $post->post_password;
  1456.         }
  1457.  
  1458.         if ( ! empty( $schema['properties']['slug'] ) ) {
  1459.             $data['slug'] = $post->post_name;
  1460.         }
  1461.  
  1462.         if ( ! empty( $schema['properties']['status'] ) ) {
  1463.             $data['status'] = $post->post_status;
  1464.         }
  1465.  
  1466.         if ( ! empty( $schema['properties']['type'] ) ) {
  1467.             $data['type'] = $post->post_type;
  1468.         }
  1469.  
  1470.         if ( ! empty( $schema['properties']['link'] ) ) {
  1471.             $data['link'] = get_permalink( $post->ID );
  1472.         }
  1473.  
  1474.         if ( ! empty( $schema['properties']['title'] ) ) {
  1475.             add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
  1476.  
  1477.             $data['title'] = array(
  1478.                 'raw'      => $post->post_title,
  1479.                 'rendered' => get_the_title( $post->ID ),
  1480.             );
  1481.  
  1482.             remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
  1483.         }
  1484.  
  1485.         $has_password_filter = false;
  1486.  
  1487.         if ( $this->can_access_password_content( $post, $request ) ) {
  1488.             // Allow access to the post, permissions already checked before.
  1489.             add_filter( 'post_password_required', '__return_false' );
  1490.  
  1491.             $has_password_filter = true;
  1492.         }
  1493.  
  1494.         if ( ! empty( $schema['properties']['content'] ) ) {
  1495.             $data['content'] = array(
  1496.                 'raw'       => $post->post_content,
  1497.                 /** This filter is documented in wp-includes/post-template.php */
  1498.                 'rendered'  => post_password_required( $post ) ? '' : apply_filters( 'the_content', $post->post_content ),
  1499.                 'protected' => (bool) $post->post_password,
  1500.             );
  1501.         }
  1502.  
  1503.         if ( ! empty( $schema['properties']['excerpt'] ) ) {
  1504.             /** This filter is documented in wp-includes/post-template.php */
  1505.             $excerpt = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $post->post_excerpt, $post ) );
  1506.             $data['excerpt'] = array(
  1507.                 'raw'       => $post->post_excerpt,
  1508.                 'rendered'  => post_password_required( $post ) ? '' : $excerpt,
  1509.                 'protected' => (bool) $post->post_password,
  1510.             );
  1511.         }
  1512.  
  1513.         if ( $has_password_filter ) {
  1514.             // Reset filter.
  1515.             remove_filter( 'post_password_required', '__return_false' );
  1516.         }
  1517.  
  1518.         if ( ! empty( $schema['properties']['author'] ) ) {
  1519.             $data['author'] = (int) $post->post_author;
  1520.         }
  1521.  
  1522.         if ( ! empty( $schema['properties']['featured_media'] ) ) {
  1523.             $data['featured_media'] = (int) get_post_thumbnail_id( $post->ID );
  1524.         }
  1525.  
  1526.         if ( ! empty( $schema['properties']['parent'] ) ) {
  1527.             $data['parent'] = (int) $post->post_parent;
  1528.         }
  1529.  
  1530.         if ( ! empty( $schema['properties']['menu_order'] ) ) {
  1531.             $data['menu_order'] = (int) $post->menu_order;
  1532.         }
  1533.  
  1534.         if ( ! empty( $schema['properties']['comment_status'] ) ) {
  1535.             $data['comment_status'] = $post->comment_status;
  1536.         }
  1537.  
  1538.         if ( ! empty( $schema['properties']['ping_status'] ) ) {
  1539.             $data['ping_status'] = $post->ping_status;
  1540.         }
  1541.  
  1542.         if ( ! empty( $schema['properties']['sticky'] ) ) {
  1543.             $data['sticky'] = is_sticky( $post->ID );
  1544.         }
  1545.  
  1546.         if ( ! empty( $schema['properties']['template'] ) ) {
  1547.             if ( $template = get_page_template_slug( $post->ID ) ) {
  1548.                 $data['template'] = $template;
  1549.             } else {
  1550.                 $data['template'] = '';
  1551.             }
  1552.         }
  1553.  
  1554.         if ( ! empty( $schema['properties']['format'] ) ) {
  1555.             $data['format'] = get_post_format( $post->ID );
  1556.  
  1557.             // Fill in blank post format.
  1558.             if ( empty( $data['format'] ) ) {
  1559.                 $data['format'] = 'standard';
  1560.             }
  1561.         }
  1562.  
  1563.         if ( ! empty( $schema['properties']['meta'] ) ) {
  1564.             $data['meta'] = $this->meta->get_value( $post->ID, $request );
  1565.         }
  1566.  
  1567.         $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
  1568.  
  1569.         foreach ( $taxonomies as $taxonomy ) {
  1570.             $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
  1571.  
  1572.             if ( ! empty( $schema['properties'][ $base ] ) ) {
  1573.                 $terms = get_the_terms( $post, $taxonomy->name );
  1574.                 $data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
  1575.             }
  1576.         }
  1577.  
  1578.         $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
  1579.         $data    = $this->add_additional_fields_to_object( $data, $request );
  1580.         $data    = $this->filter_response_by_context( $data, $context );
  1581.  
  1582.         // Wrap the data in a response object.
  1583.         $response = rest_ensure_response( $data );
  1584.  
  1585.         $response->add_links( $this->prepare_links( $post ) );
  1586.  
  1587.         /**
  1588.          * Filters the post data for a response.
  1589.          *
  1590.          * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
  1591.          *
  1592.          * @since 4.7.0
  1593.          *
  1594.          * @param WP_REST_Response $response The response object.
  1595.          * @param WP_Post          $post     Post object.
  1596.          * @param WP_REST_Request  $request  Request object.
  1597.          */
  1598.         return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
  1599.     }
  1600.  
  1601.     /**
  1602.      * Overwrites the default protected title format.
  1603.      *
  1604.      * By default, WordPress will show password protected posts with a title of
  1605.      * "Protected: %s", as the REST API communicates the protected status of a post
  1606.      * in a machine readable format, we remove the "Protected: " prefix.
  1607.      *
  1608.      * @since 4.7.0
  1609.      *
  1610.      * @return string Protected title format.
  1611.      */
  1612.     public function protected_title_format() {
  1613.         return '%s';
  1614.     }
  1615.  
  1616.     /**
  1617.      * Prepares links for the request.
  1618.      *
  1619.      * @since 4.7.0
  1620.      *
  1621.      * @param WP_Post $post Post object.
  1622.      * @return array Links for the given post.
  1623.      */
  1624.     protected function prepare_links( $post ) {
  1625.         $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
  1626.  
  1627.         // Entity meta.
  1628.         $links = array(
  1629.             'self' => array(
  1630.                 'href'   => rest_url( trailingslashit( $base ) . $post->ID ),
  1631.             ),
  1632.             'collection' => array(
  1633.                 'href'   => rest_url( $base ),
  1634.             ),
  1635.             'about'      => array(
  1636.                 'href'   => rest_url( 'wp/v2/types/' . $this->post_type ),
  1637.             ),
  1638.         );
  1639.  
  1640.         if ( ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'author' ) )
  1641.             && ! empty( $post->post_author ) ) {
  1642.             $links['author'] = array(
  1643.                 'href'       => rest_url( 'wp/v2/users/' . $post->post_author ),
  1644.                 'embeddable' => true,
  1645.             );
  1646.         }
  1647.  
  1648.         if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'comments' ) ) {
  1649.             $replies_url = rest_url( 'wp/v2/comments' );
  1650.             $replies_url = add_query_arg( 'post', $post->ID, $replies_url );
  1651.  
  1652.             $links['replies'] = array(
  1653.                 'href'       => $replies_url,
  1654.                 'embeddable' => true,
  1655.             );
  1656.         }
  1657.  
  1658.         if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'revisions' ) ) {
  1659.             $links['version-history'] = array(
  1660.                 'href' => rest_url( trailingslashit( $base ) . $post->ID . '/revisions' ),
  1661.             );
  1662.         }
  1663.  
  1664.         $post_type_obj = get_post_type_object( $post->post_type );
  1665.  
  1666.         if ( $post_type_obj->hierarchical && ! empty( $post->post_parent ) ) {
  1667.             $links['up'] = array(
  1668.                 'href'       => rest_url( trailingslashit( $base ) . (int) $post->post_parent ),
  1669.                 'embeddable' => true,
  1670.             );
  1671.         }
  1672.  
  1673.         // If we have a featured media, add that.
  1674.         if ( $featured_media = get_post_thumbnail_id( $post->ID ) ) {
  1675.             $image_url = rest_url( 'wp/v2/media/' . $featured_media );
  1676.  
  1677.             $links['https://api.w.org/featuredmedia'] = array(
  1678.                 'href'       => $image_url,
  1679.                 'embeddable' => true,
  1680.             );
  1681.         }
  1682.  
  1683.         if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ), true ) ) {
  1684.             $attachments_url = rest_url( 'wp/v2/media' );
  1685.             $attachments_url = add_query_arg( 'parent', $post->ID, $attachments_url );
  1686.  
  1687.             $links['https://api.w.org/attachment'] = array(
  1688.                 'href' => $attachments_url,
  1689.             );
  1690.         }
  1691.  
  1692.         $taxonomies = get_object_taxonomies( $post->post_type );
  1693.  
  1694.         if ( ! empty( $taxonomies ) ) {
  1695.             $links['https://api.w.org/term'] = array();
  1696.  
  1697.             foreach ( $taxonomies as $tax ) {
  1698.                 $taxonomy_obj = get_taxonomy( $tax );
  1699.  
  1700.                 // Skip taxonomies that are not public.
  1701.                 if ( empty( $taxonomy_obj->show_in_rest ) ) {
  1702.                     continue;
  1703.                 }
  1704.  
  1705.                 $tax_base = ! empty( $taxonomy_obj->rest_base ) ? $taxonomy_obj->rest_base : $tax;
  1706.  
  1707.                 $terms_url = add_query_arg(
  1708.                     'post',
  1709.                     $post->ID,
  1710.                     rest_url( 'wp/v2/' . $tax_base )
  1711.                 );
  1712.  
  1713.                 $links['https://api.w.org/term'][] = array(
  1714.                     'href'       => $terms_url,
  1715.                     'taxonomy'   => $tax,
  1716.                     'embeddable' => true,
  1717.                 );
  1718.             }
  1719.         }
  1720.  
  1721.         return $links;
  1722.     }
  1723.  
  1724.     /**
  1725.      * Retrieves the post's schema, conforming to JSON Schema.
  1726.      *
  1727.      * @since 4.7.0
  1728.      *
  1729.      * @return array Item schema data.
  1730.      */
  1731.     public function get_item_schema() {
  1732.  
  1733.         $schema = array(
  1734.             '$schema'    => 'http://json-schema.org/draft-04/schema#',
  1735.             'title'      => $this->post_type,
  1736.             'type'       => 'object',
  1737.             // Base properties for every Post.
  1738.             'properties' => array(
  1739.                 'date'            => array(
  1740.                     'description' => __( "The date the object was published, in the site's timezone." ),
  1741.                     'type'        => 'string',
  1742.                     'format'      => 'date-time',
  1743.                     'context'     => array( 'view', 'edit', 'embed' ),
  1744.                 ),
  1745.                 'date_gmt'        => array(
  1746.                     'description' => __( 'The date the object was published, as GMT.' ),
  1747.                     'type'        => 'string',
  1748.                     'format'      => 'date-time',
  1749.                     'context'     => array( 'view', 'edit' ),
  1750.                 ),
  1751.                 'guid'            => array(
  1752.                     'description' => __( 'The globally unique identifier for the object.' ),
  1753.                     'type'        => 'object',
  1754.                     'context'     => array( 'view', 'edit' ),
  1755.                     'readonly'    => true,
  1756.                     'properties'  => array(
  1757.                         'raw'      => array(
  1758.                             'description' => __( 'GUID for the object, as it exists in the database.' ),
  1759.                             'type'        => 'string',
  1760.                             'context'     => array( 'edit' ),
  1761.                             'readonly'    => true,
  1762.                         ),
  1763.                         'rendered' => array(
  1764.                             'description' => __( 'GUID for the object, transformed for display.' ),
  1765.                             'type'        => 'string',
  1766.                             'context'     => array( 'view', 'edit' ),
  1767.                             'readonly'    => true,
  1768.                         ),
  1769.                     ),
  1770.                 ),
  1771.                 'id'              => array(
  1772.                     'description' => __( 'Unique identifier for the object.' ),
  1773.                     'type'        => 'integer',
  1774.                     'context'     => array( 'view', 'edit', 'embed' ),
  1775.                     'readonly'    => true,
  1776.                 ),
  1777.                 'link'            => array(
  1778.                     'description' => __( 'URL to the object.' ),
  1779.                     'type'        => 'string',
  1780.                     'format'      => 'uri',
  1781.                     'context'     => array( 'view', 'edit', 'embed' ),
  1782.                     'readonly'    => true,
  1783.                 ),
  1784.                 'modified'        => array(
  1785.                     'description' => __( "The date the object was last modified, in the site's timezone." ),
  1786.                     'type'        => 'string',
  1787.                     'format'      => 'date-time',
  1788.                     'context'     => array( 'view', 'edit' ),
  1789.                     'readonly'    => true,
  1790.                 ),
  1791.                 'modified_gmt'    => array(
  1792.                     'description' => __( 'The date the object was last modified, as GMT.' ),
  1793.                     'type'        => 'string',
  1794.                     'format'      => 'date-time',
  1795.                     'context'     => array( 'view', 'edit' ),
  1796.                     'readonly'    => true,
  1797.                 ),
  1798.                 'slug'            => array(
  1799.                     'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
  1800.                     'type'        => 'string',
  1801.                     'context'     => array( 'view', 'edit', 'embed' ),
  1802.                     'arg_options' => array(
  1803.                         'sanitize_callback' => array( $this, 'sanitize_slug' ),
  1804.                     ),
  1805.                 ),
  1806.                 'status'          => array(
  1807.                     'description' => __( 'A named status for the object.' ),
  1808.                     'type'        => 'string',
  1809.                     'enum'        => array_keys( get_post_stati( array( 'internal' => false ) ) ),
  1810.                     'context'     => array( 'view', 'edit' ),
  1811.                 ),
  1812.                 'type'            => array(
  1813.                     'description' => __( 'Type of Post for the object.' ),
  1814.                     'type'        => 'string',
  1815.                     'context'     => array( 'view', 'edit', 'embed' ),
  1816.                     'readonly'    => true,
  1817.                 ),
  1818.                 'password'        => array(
  1819.                     'description' => __( 'A password to protect access to the content and excerpt.' ),
  1820.                     'type'        => 'string',
  1821.                     'context'     => array( 'edit' ),
  1822.                 ),
  1823.             ),
  1824.         );
  1825.  
  1826.         $post_type_obj = get_post_type_object( $this->post_type );
  1827.  
  1828.         if ( $post_type_obj->hierarchical ) {
  1829.             $schema['properties']['parent'] = array(
  1830.                 'description' => __( 'The ID for the parent of the object.' ),
  1831.                 'type'        => 'integer',
  1832.                 'context'     => array( 'view', 'edit' ),
  1833.             );
  1834.         }
  1835.  
  1836.         $post_type_attributes = array(
  1837.             'title',
  1838.             'editor',
  1839.             'author',
  1840.             'excerpt',
  1841.             'thumbnail',
  1842.             'comments',
  1843.             'revisions',
  1844.             'page-attributes',
  1845.             'post-formats',
  1846.             'custom-fields',
  1847.         );
  1848.         $fixed_schemas = array(
  1849.             'post' => array(
  1850.                 'title',
  1851.                 'editor',
  1852.                 'author',
  1853.                 'excerpt',
  1854.                 'thumbnail',
  1855.                 'comments',
  1856.                 'revisions',
  1857.                 'post-formats',
  1858.                 'custom-fields',
  1859.             ),
  1860.             'page' => array(
  1861.                 'title',
  1862.                 'editor',
  1863.                 'author',
  1864.                 'excerpt',
  1865.                 'thumbnail',
  1866.                 'comments',
  1867.                 'revisions',
  1868.                 'page-attributes',
  1869.                 'custom-fields',
  1870.             ),
  1871.             'attachment' => array(
  1872.                 'title',
  1873.                 'author',
  1874.                 'comments',
  1875.                 'revisions',
  1876.                 'custom-fields',
  1877.             ),
  1878.         );
  1879.         foreach ( $post_type_attributes as $attribute ) {
  1880.             if ( isset( $fixed_schemas[ $this->post_type ] ) && ! in_array( $attribute, $fixed_schemas[ $this->post_type ], true ) ) {
  1881.                 continue;
  1882.             } elseif ( ! isset( $fixed_schemas[ $this->post_type ] ) && ! post_type_supports( $this->post_type, $attribute ) ) {
  1883.                 continue;
  1884.             }
  1885.  
  1886.             switch ( $attribute ) {
  1887.  
  1888.                 case 'title':
  1889.                     $schema['properties']['title'] = array(
  1890.                         'description' => __( 'The title for the object.' ),
  1891.                         'type'        => 'object',
  1892.                         'context'     => array( 'view', 'edit', 'embed' ),
  1893.                         'arg_options' => array(
  1894.                             'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database()
  1895.                             'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database()
  1896.                         ),
  1897.                         'properties'  => array(
  1898.                             'raw' => array(
  1899.                                 'description' => __( 'Title for the object, as it exists in the database.' ),
  1900.                                 'type'        => 'string',
  1901.                                 'context'     => array( 'edit' ),
  1902.                             ),
  1903.                             'rendered' => array(
  1904.                                 'description' => __( 'HTML title for the object, transformed for display.' ),
  1905.                                 'type'        => 'string',
  1906.                                 'context'     => array( 'view', 'edit', 'embed' ),
  1907.                                 'readonly'    => true,
  1908.                             ),
  1909.                         ),
  1910.                     );
  1911.                     break;
  1912.  
  1913.                 case 'editor':
  1914.                     $schema['properties']['content'] = array(
  1915.                         'description' => __( 'The content for the object.' ),
  1916.                         'type'        => 'object',
  1917.                         'context'     => array( 'view', 'edit' ),
  1918.                         'arg_options' => array(
  1919.                             'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database()
  1920.                             'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database()
  1921.                         ),
  1922.                         'properties'  => array(
  1923.                             'raw' => array(
  1924.                                 'description' => __( 'Content for the object, as it exists in the database.' ),
  1925.                                 'type'        => 'string',
  1926.                                 'context'     => array( 'edit' ),
  1927.                             ),
  1928.                             'rendered' => array(
  1929.                                 'description' => __( 'HTML content for the object, transformed for display.' ),
  1930.                                 'type'        => 'string',
  1931.                                 'context'     => array( 'view', 'edit' ),
  1932.                                 'readonly'    => true,
  1933.                             ),
  1934.                             'protected'       => array(
  1935.                                 'description' => __( 'Whether the content is protected with a password.' ),
  1936.                                 'type'        => 'boolean',
  1937.                                 'context'     => array( 'view', 'edit', 'embed' ),
  1938.                                 'readonly'    => true,
  1939.                             ),
  1940.                         ),
  1941.                     );
  1942.                     break;
  1943.  
  1944.                 case 'author':
  1945.                     $schema['properties']['author'] = array(
  1946.                         'description' => __( 'The ID for the author of the object.' ),
  1947.                         'type'        => 'integer',
  1948.                         'context'     => array( 'view', 'edit', 'embed' ),
  1949.                     );
  1950.                     break;
  1951.  
  1952.                 case 'excerpt':
  1953.                     $schema['properties']['excerpt'] = array(
  1954.                         'description' => __( 'The excerpt for the object.' ),
  1955.                         'type'        => 'object',
  1956.                         'context'     => array( 'view', 'edit', 'embed' ),
  1957.                         'arg_options' => array(
  1958.                             'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database()
  1959.                             'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database()
  1960.                         ),
  1961.                         'properties'  => array(
  1962.                             'raw' => array(
  1963.                                 'description' => __( 'Excerpt for the object, as it exists in the database.' ),
  1964.                                 'type'        => 'string',
  1965.                                 'context'     => array( 'edit' ),
  1966.                             ),
  1967.                             'rendered' => array(
  1968.                                 'description' => __( 'HTML excerpt for the object, transformed for display.' ),
  1969.                                 'type'        => 'string',
  1970.                                 'context'     => array( 'view', 'edit', 'embed' ),
  1971.                                 'readonly'    => true,
  1972.                             ),
  1973.                             'protected'       => array(
  1974.                                 'description' => __( 'Whether the excerpt is protected with a password.' ),
  1975.                                 'type'        => 'boolean',
  1976.                                 'context'     => array( 'view', 'edit', 'embed' ),
  1977.                                 'readonly'    => true,
  1978.                             ),
  1979.                         ),
  1980.                     );
  1981.                     break;
  1982.  
  1983.                 case 'thumbnail':
  1984.                     $schema['properties']['featured_media'] = array(
  1985.                         'description' => __( 'The ID of the featured media for the object.' ),
  1986.                         'type'        => 'integer',
  1987.                         'context'     => array( 'view', 'edit', 'embed' ),
  1988.                     );
  1989.                     break;
  1990.  
  1991.                 case 'comments':
  1992.                     $schema['properties']['comment_status'] = array(
  1993.                         'description' => __( 'Whether or not comments are open on the object.' ),
  1994.                         'type'        => 'string',
  1995.                         'enum'        => array( 'open', 'closed' ),
  1996.                         'context'     => array( 'view', 'edit' ),
  1997.                     );
  1998.                     $schema['properties']['ping_status'] = array(
  1999.                         'description' => __( 'Whether or not the object can be pinged.' ),
  2000.                         'type'        => 'string',
  2001.                         'enum'        => array( 'open', 'closed' ),
  2002.                         'context'     => array( 'view', 'edit' ),
  2003.                     );
  2004.                     break;
  2005.  
  2006.                 case 'page-attributes':
  2007.                     $schema['properties']['menu_order'] = array(
  2008.                         'description' => __( 'The order of the object in relation to other object of its type.' ),
  2009.                         'type'        => 'integer',
  2010.                         'context'     => array( 'view', 'edit' ),
  2011.                     );
  2012.                     break;
  2013.  
  2014.                 case 'post-formats':
  2015.                     // Get the native post formats and remove the array keys.
  2016.                     $formats = array_values( get_post_format_slugs() );
  2017.  
  2018.                     $schema['properties']['format'] = array(
  2019.                         'description' => __( 'The format for the object.' ),
  2020.                         'type'        => 'string',
  2021.                         'enum'        => $formats,
  2022.                         'context'     => array( 'view', 'edit' ),
  2023.                     );
  2024.                     break;
  2025.  
  2026.                 case 'custom-fields':
  2027.                     $schema['properties']['meta'] = $this->meta->get_field_schema();
  2028.                     break;
  2029.  
  2030.             }
  2031.         }
  2032.  
  2033.         if ( 'post' === $this->post_type ) {
  2034.             $schema['properties']['sticky'] = array(
  2035.                 'description' => __( 'Whether or not the object should be treated as sticky.' ),
  2036.                 'type'        => 'boolean',
  2037.                 'context'     => array( 'view', 'edit' ),
  2038.             );
  2039.         }
  2040.  
  2041.         $schema['properties']['template'] = array(
  2042.             'description' => __( 'The theme file to use to display the object.' ),
  2043.             'type'        => 'string',
  2044.             'context'     => array( 'view', 'edit' ),
  2045.             'arg_options' => array(
  2046.                 'validate_callback' => array( $this, 'check_template' ),
  2047.             ),
  2048.         );
  2049.  
  2050.         $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
  2051.         foreach ( $taxonomies as $taxonomy ) {
  2052.             $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
  2053.             $schema['properties'][ $base ] = array(
  2054.                 /* translators: %s: taxonomy name */
  2055.                 'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
  2056.                 'type'        => 'array',
  2057.                 'items'       => array(
  2058.                     'type'    => 'integer',
  2059.                 ),
  2060.                 'context'     => array( 'view', 'edit' ),
  2061.             );
  2062.         }
  2063.  
  2064.         return $this->add_additional_fields_schema( $schema );
  2065.     }
  2066.  
  2067.     /**
  2068.      * Retrieves the query params for the posts collection.
  2069.      *
  2070.      * @since 4.7.0
  2071.      *
  2072.      * @return array Collection parameters.
  2073.      */
  2074.     public function get_collection_params() {
  2075.         $query_params = parent::get_collection_params();
  2076.  
  2077.         $query_params['context']['default'] = 'view';
  2078.  
  2079.         $query_params['after'] = array(
  2080.             'description'        => __( 'Limit response to posts published after a given ISO8601 compliant date.' ),
  2081.             'type'               => 'string',
  2082.             'format'             => 'date-time',
  2083.         );
  2084.  
  2085.         if ( post_type_supports( $this->post_type, 'author' ) ) {
  2086.             $query_params['author'] = array(
  2087.                 'description'         => __( 'Limit result set to posts assigned to specific authors.' ),
  2088.                 'type'                => 'array',
  2089.                 'items'               => array(
  2090.                     'type'            => 'integer',
  2091.                 ),
  2092.                 'default'             => array(),
  2093.             );
  2094.             $query_params['author_exclude'] = array(
  2095.                 'description'         => __( 'Ensure result set excludes posts assigned to specific authors.' ),
  2096.                 'type'                => 'array',
  2097.                 'items'               => array(
  2098.                     'type'            => 'integer',
  2099.                 ),
  2100.                 'default'             => array(),
  2101.             );
  2102.         }
  2103.  
  2104.         $query_params['before'] = array(
  2105.             'description'        => __( 'Limit response to posts published before a given ISO8601 compliant date.' ),
  2106.             'type'               => 'string',
  2107.             'format'             => 'date-time',
  2108.         );
  2109.  
  2110.         $query_params['exclude'] = array(
  2111.             'description'        => __( 'Ensure result set excludes specific IDs.' ),
  2112.             'type'               => 'array',
  2113.             'items'              => array(
  2114.                 'type'           => 'integer',
  2115.             ),
  2116.             'default'            => array(),
  2117.         );
  2118.  
  2119.         $query_params['include'] = array(
  2120.             'description'        => __( 'Limit result set to specific IDs.' ),
  2121.             'type'               => 'array',
  2122.             'items'              => array(
  2123.                 'type'           => 'integer',
  2124.             ),
  2125.             'default'            => array(),
  2126.         );
  2127.  
  2128.         if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
  2129.             $query_params['menu_order'] = array(
  2130.                 'description'        => __( 'Limit result set to posts with a specific menu_order value.' ),
  2131.                 'type'               => 'integer',
  2132.             );
  2133.         }
  2134.  
  2135.         $query_params['offset'] = array(
  2136.             'description'        => __( 'Offset the result set by a specific number of items.' ),
  2137.             'type'               => 'integer',
  2138.         );
  2139.  
  2140.         $query_params['order'] = array(
  2141.             'description'        => __( 'Order sort attribute ascending or descending.' ),
  2142.             'type'               => 'string',
  2143.             'default'            => 'desc',
  2144.             'enum'               => array( 'asc', 'desc' ),
  2145.         );
  2146.  
  2147.         $query_params['orderby'] = array(
  2148.             'description'        => __( 'Sort collection by object attribute.' ),
  2149.             'type'               => 'string',
  2150.             'default'            => 'date',
  2151.             'enum'               => array(
  2152.                 'author',
  2153.                 'date',
  2154.                 'id',
  2155.                 'include',
  2156.                 'modified',
  2157.                 'parent',
  2158.                 'relevance',
  2159.                 'slug',
  2160.                 'include_slugs',
  2161.                 'title',
  2162.             ),
  2163.         );
  2164.  
  2165.         if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
  2166.             $query_params['orderby']['enum'][] = 'menu_order';
  2167.         }
  2168.  
  2169.         $post_type = get_post_type_object( $this->post_type );
  2170.  
  2171.         if ( $post_type->hierarchical || 'attachment' === $this->post_type ) {
  2172.             $query_params['parent'] = array(
  2173.                 'description'       => __( 'Limit result set to items with particular parent IDs.' ),
  2174.                 'type'              => 'array',
  2175.                 'items'             => array(
  2176.                     'type'          => 'integer',
  2177.                 ),
  2178.                 'default'           => array(),
  2179.             );
  2180.             $query_params['parent_exclude'] = array(
  2181.                 'description'       => __( 'Limit result set to all items except those of a particular parent ID.' ),
  2182.                 'type'              => 'array',
  2183.                 'items'             => array(
  2184.                     'type'          => 'integer',
  2185.                 ),
  2186.                 'default'           => array(),
  2187.             );
  2188.         }
  2189.  
  2190.         $query_params['slug'] = array(
  2191.             'description'       => __( 'Limit result set to posts with one or more specific slugs.' ),
  2192.             'type'              => 'array',
  2193.             'items'             => array(
  2194.                 'type'          => 'string',
  2195.             ),
  2196.             'sanitize_callback' => 'wp_parse_slug_list',
  2197.         );
  2198.  
  2199.         $query_params['status'] = array(
  2200.             'default'           => 'publish',
  2201.             'description'       => __( 'Limit result set to posts assigned one or more statuses.' ),
  2202.             'type'              => 'array',
  2203.             'items'             => array(
  2204.                 'enum'          => array_merge( array_keys( get_post_stati() ), array( 'any' ) ),
  2205.                 'type'          => 'string',
  2206.             ),
  2207.             'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
  2208.         );
  2209.  
  2210.         $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
  2211.  
  2212.         foreach ( $taxonomies as $taxonomy ) {
  2213.             $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
  2214.  
  2215.             $query_params[ $base ] = array(
  2216.                 /* translators: %s: taxonomy name */
  2217.                 'description'       => sprintf( __( 'Limit result set to all items that have the specified term assigned in the %s taxonomy.' ), $base ),
  2218.                 'type'              => 'array',
  2219.                 'items'             => array(
  2220.                     'type'          => 'integer',
  2221.                 ),
  2222.                 'default'           => array(),
  2223.             );
  2224.  
  2225.             $query_params[ $base . '_exclude' ] = array(
  2226.                 /* translators: %s: taxonomy name */
  2227.                 'description' => sprintf( __( 'Limit result set to all items except those that have the specified term assigned in the %s taxonomy.' ), $base ),
  2228.                 'type'        => 'array',
  2229.                 'items'       => array(
  2230.                     'type'    => 'integer',
  2231.                 ),
  2232.                 'default'           => array(),
  2233.             );
  2234.         }
  2235.  
  2236.         if ( 'post' === $this->post_type ) {
  2237.             $query_params['sticky'] = array(
  2238.                 'description'       => __( 'Limit result set to items that are sticky.' ),
  2239.                 'type'              => 'boolean',
  2240.             );
  2241.         }
  2242.  
  2243.         /**
  2244.          * Filter collection parameters for the posts controller.
  2245.          *
  2246.          * The dynamic part of the filter `$this->post_type` refers to the post
  2247.          * type slug for the controller.
  2248.          *
  2249.          * This filter registers the collection parameter, but does not map the
  2250.          * collection parameter to an internal WP_Query parameter. Use the
  2251.          * `rest_{$this->post_type}_query` filter to set WP_Query parameters.
  2252.          *
  2253.          * @since 4.7.0
  2254.          *
  2255.          * @param array        $query_params JSON Schema-formatted collection parameters.
  2256.          * @param WP_Post_Type $post_type    Post type object.
  2257.          */
  2258.         return apply_filters( "rest_{$this->post_type}_collection_params", $query_params, $post_type );
  2259.     }
  2260.  
  2261.     /**
  2262.      * Sanitizes and validates the list of post statuses, including whether the
  2263.      * user can query private statuses.
  2264.      *
  2265.      * @since 4.7.0
  2266.      *
  2267.      * @param  string|array    $statuses  One or more post statuses.
  2268.      * @param  WP_REST_Request $request   Full details about the request.
  2269.      * @param  string          $parameter Additional parameter to pass to validation.
  2270.      * @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
  2271.      */
  2272.     public function sanitize_post_statuses( $statuses, $request, $parameter ) {
  2273.         $statuses = wp_parse_slug_list( $statuses );
  2274.  
  2275.         // The default status is different in WP_REST_Attachments_Controller
  2276.         $attributes = $request->get_attributes();
  2277.         $default_status = $attributes['args']['status']['default'];
  2278.  
  2279.         foreach ( $statuses as $status ) {
  2280.             if ( $status === $default_status ) {
  2281.                 continue;
  2282.             }
  2283.  
  2284.             $post_type_obj = get_post_type_object( $this->post_type );
  2285.  
  2286.             if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
  2287.                 $result = rest_validate_request_arg( $status, $request, $parameter );
  2288.                 if ( is_wp_error( $result ) ) {
  2289.                     return $result;
  2290.                 }
  2291.             } else {
  2292.                 return new WP_Error( 'rest_forbidden_status', __( 'Status is forbidden.' ), array( 'status' => rest_authorization_required_code() ) );
  2293.             }
  2294.         }
  2295.  
  2296.         return $statuses;
  2297.     }
  2298. }
  2299.