Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 

1636 строки
52 KiB

  1. <?php
  2. /**
  3. * REST API: WP_REST_Comments_Controller class
  4. *
  5. * @package WordPress
  6. * @subpackage REST_API
  7. * @since 4.7.0
  8. */
  9. /**
  10. * Core controller used to access comments via the REST API.
  11. *
  12. * @since 4.7.0
  13. *
  14. * @see WP_REST_Controller
  15. */
  16. class WP_REST_Comments_Controller extends WP_REST_Controller {
  17. /**
  18. * Instance of a comment meta fields object.
  19. *
  20. * @since 4.7.0
  21. * @access protected
  22. * @var WP_REST_Comment_Meta_Fields
  23. */
  24. protected $meta;
  25. /**
  26. * Constructor.
  27. *
  28. * @since 4.7.0
  29. * @access public
  30. */
  31. public function __construct() {
  32. $this->namespace = 'wp/v2';
  33. $this->rest_base = 'comments';
  34. $this->meta = new WP_REST_Comment_Meta_Fields();
  35. }
  36. /**
  37. * Registers the routes for the objects of the controller.
  38. *
  39. * @since 4.7.0
  40. * @access public
  41. */
  42. public function register_routes() {
  43. register_rest_route( $this->namespace, '/' . $this->rest_base, array(
  44. array(
  45. 'methods' => WP_REST_Server::READABLE,
  46. 'callback' => array( $this, 'get_items' ),
  47. 'permission_callback' => array( $this, 'get_items_permissions_check' ),
  48. 'args' => $this->get_collection_params(),
  49. ),
  50. array(
  51. 'methods' => WP_REST_Server::CREATABLE,
  52. 'callback' => array( $this, 'create_item' ),
  53. 'permission_callback' => array( $this, 'create_item_permissions_check' ),
  54. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
  55. ),
  56. 'schema' => array( $this, 'get_public_item_schema' ),
  57. ) );
  58. register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
  59. 'args' => array(
  60. 'id' => array(
  61. 'description' => __( 'Unique identifier for the object.' ),
  62. 'type' => 'integer',
  63. ),
  64. ),
  65. array(
  66. 'methods' => WP_REST_Server::READABLE,
  67. 'callback' => array( $this, 'get_item' ),
  68. 'permission_callback' => array( $this, 'get_item_permissions_check' ),
  69. 'args' => array(
  70. 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
  71. 'password' => array(
  72. 'description' => __( 'The password for the post if it is password protected.' ),
  73. 'type' => 'string',
  74. ),
  75. ),
  76. ),
  77. array(
  78. 'methods' => WP_REST_Server::EDITABLE,
  79. 'callback' => array( $this, 'update_item' ),
  80. 'permission_callback' => array( $this, 'update_item_permissions_check' ),
  81. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  82. ),
  83. array(
  84. 'methods' => WP_REST_Server::DELETABLE,
  85. 'callback' => array( $this, 'delete_item' ),
  86. 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
  87. 'args' => array(
  88. 'force' => array(
  89. 'type' => 'boolean',
  90. 'default' => false,
  91. 'description' => __( 'Whether to bypass trash and force deletion.' ),
  92. ),
  93. 'password' => array(
  94. 'description' => __( 'The password for the post if it is password protected.' ),
  95. 'type' => 'string',
  96. ),
  97. ),
  98. ),
  99. 'schema' => array( $this, 'get_public_item_schema' ),
  100. ) );
  101. }
  102. /**
  103. * Checks if a given request has access to read comments.
  104. *
  105. * @since 4.7.0
  106. * @access public
  107. *
  108. * @param WP_REST_Request $request Full details about the request.
  109. * @return WP_Error|bool True if the request has read access, error object otherwise.
  110. */
  111. public function get_items_permissions_check( $request ) {
  112. if ( ! empty( $request['post'] ) ) {
  113. foreach ( (array) $request['post'] as $post_id ) {
  114. $post = get_post( $post_id );
  115. if ( ! empty( $post_id ) && $post && ! $this->check_read_post_permission( $post, $request ) ) {
  116. return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you are not allowed to read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) );
  117. } elseif ( 0 === $post_id && ! current_user_can( 'moderate_comments' ) ) {
  118. return new WP_Error( 'rest_cannot_read', __( 'Sorry, you are not allowed to read comments without a post.' ), array( 'status' => rest_authorization_required_code() ) );
  119. }
  120. }
  121. }
  122. if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
  123. return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit comments.' ), array( 'status' => rest_authorization_required_code() ) );
  124. }
  125. if ( ! current_user_can( 'edit_posts' ) ) {
  126. $protected_params = array( 'author', 'author_exclude', 'author_email', 'type', 'status' );
  127. $forbidden_params = array();
  128. foreach ( $protected_params as $param ) {
  129. if ( 'status' === $param ) {
  130. if ( 'approve' !== $request[ $param ] ) {
  131. $forbidden_params[] = $param;
  132. }
  133. } elseif ( 'type' === $param ) {
  134. if ( 'comment' !== $request[ $param ] ) {
  135. $forbidden_params[] = $param;
  136. }
  137. } elseif ( ! empty( $request[ $param ] ) ) {
  138. $forbidden_params[] = $param;
  139. }
  140. }
  141. if ( ! empty( $forbidden_params ) ) {
  142. return new WP_Error( 'rest_forbidden_param', sprintf( __( 'Query parameter not permitted: %s' ), implode( ', ', $forbidden_params ) ), array( 'status' => rest_authorization_required_code() ) );
  143. }
  144. }
  145. return true;
  146. }
  147. /**
  148. * Retrieves a list of comment items.
  149. *
  150. * @since 4.7.0
  151. * @access public
  152. *
  153. * @param WP_REST_Request $request Full details about the request.
  154. * @return WP_Error|WP_REST_Response Response object on success, or error object on failure.
  155. */
  156. public function get_items( $request ) {
  157. // Retrieve the list of registered collection query parameters.
  158. $registered = $this->get_collection_params();
  159. /*
  160. * This array defines mappings between public API query parameters whose
  161. * values are accepted as-passed, and their internal WP_Query parameter
  162. * name equivalents (some are the same). Only values which are also
  163. * present in $registered will be set.
  164. */
  165. $parameter_mappings = array(
  166. 'author' => 'author__in',
  167. 'author_email' => 'author_email',
  168. 'author_exclude' => 'author__not_in',
  169. 'exclude' => 'comment__not_in',
  170. 'include' => 'comment__in',
  171. 'offset' => 'offset',
  172. 'order' => 'order',
  173. 'parent' => 'parent__in',
  174. 'parent_exclude' => 'parent__not_in',
  175. 'per_page' => 'number',
  176. 'post' => 'post__in',
  177. 'search' => 'search',
  178. 'status' => 'status',
  179. 'type' => 'type',
  180. );
  181. $prepared_args = array();
  182. /*
  183. * For each known parameter which is both registered and present in the request,
  184. * set the parameter's value on the query $prepared_args.
  185. */
  186. foreach ( $parameter_mappings as $api_param => $wp_param ) {
  187. if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
  188. $prepared_args[ $wp_param ] = $request[ $api_param ];
  189. }
  190. }
  191. // Ensure certain parameter values default to empty strings.
  192. foreach ( array( 'author_email', 'search' ) as $param ) {
  193. if ( ! isset( $prepared_args[ $param ] ) ) {
  194. $prepared_args[ $param ] = '';
  195. }
  196. }
  197. if ( isset( $registered['orderby'] ) ) {
  198. $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] );
  199. }
  200. $prepared_args['no_found_rows'] = false;
  201. $prepared_args['date_query'] = array();
  202. // Set before into date query. Date query must be specified as an array of an array.
  203. if ( isset( $registered['before'], $request['before'] ) ) {
  204. $prepared_args['date_query'][0]['before'] = $request['before'];
  205. }
  206. // Set after into date query. Date query must be specified as an array of an array.
  207. if ( isset( $registered['after'], $request['after'] ) ) {
  208. $prepared_args['date_query'][0]['after'] = $request['after'];
  209. }
  210. if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) {
  211. $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 );
  212. }
  213. /**
  214. * Filters arguments, before passing to WP_Comment_Query, when querying comments via the REST API.
  215. *
  216. * @since 4.7.0
  217. *
  218. * @link https://developer.wordpress.org/reference/classes/wp_comment_query/
  219. *
  220. * @param array $prepared_args Array of arguments for WP_Comment_Query.
  221. * @param WP_REST_Request $request The current request.
  222. */
  223. $prepared_args = apply_filters( 'rest_comment_query', $prepared_args, $request );
  224. $query = new WP_Comment_Query;
  225. $query_result = $query->query( $prepared_args );
  226. $comments = array();
  227. foreach ( $query_result as $comment ) {
  228. if ( ! $this->check_read_permission( $comment, $request ) ) {
  229. continue;
  230. }
  231. $data = $this->prepare_item_for_response( $comment, $request );
  232. $comments[] = $this->prepare_response_for_collection( $data );
  233. }
  234. $total_comments = (int) $query->found_comments;
  235. $max_pages = (int) $query->max_num_pages;
  236. if ( $total_comments < 1 ) {
  237. // Out-of-bounds, run the query again without LIMIT for total count.
  238. unset( $prepared_args['number'], $prepared_args['offset'] );
  239. $query = new WP_Comment_Query;
  240. $prepared_args['count'] = true;
  241. $total_comments = $query->query( $prepared_args );
  242. $max_pages = ceil( $total_comments / $request['per_page'] );
  243. }
  244. $response = rest_ensure_response( $comments );
  245. $response->header( 'X-WP-Total', $total_comments );
  246. $response->header( 'X-WP-TotalPages', $max_pages );
  247. $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
  248. if ( $request['page'] > 1 ) {
  249. $prev_page = $request['page'] - 1;
  250. if ( $prev_page > $max_pages ) {
  251. $prev_page = $max_pages;
  252. }
  253. $prev_link = add_query_arg( 'page', $prev_page, $base );
  254. $response->link_header( 'prev', $prev_link );
  255. }
  256. if ( $max_pages > $request['page'] ) {
  257. $next_page = $request['page'] + 1;
  258. $next_link = add_query_arg( 'page', $next_page, $base );
  259. $response->link_header( 'next', $next_link );
  260. }
  261. return $response;
  262. }
  263. /**
  264. * Get the comment, if the ID is valid.
  265. *
  266. * @since 4.7.2
  267. *
  268. * @param int $id Supplied ID.
  269. * @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise.
  270. */
  271. protected function get_comment( $id ) {
  272. $error = new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) );
  273. if ( (int) $id <= 0 ) {
  274. return $error;
  275. }
  276. $id = (int) $id;
  277. $comment = get_comment( $id );
  278. if ( empty( $comment ) ) {
  279. return $error;
  280. }
  281. if ( ! empty( $comment->comment_post_ID ) ) {
  282. $post = get_post( (int) $comment->comment_post_ID );
  283. if ( empty( $post ) ) {
  284. return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
  285. }
  286. }
  287. return $comment;
  288. }
  289. /**
  290. * Checks if a given request has access to read the comment.
  291. *
  292. * @since 4.7.0
  293. * @access public
  294. *
  295. * @param WP_REST_Request $request Full details about the request.
  296. * @return WP_Error|bool True if the request has read access for the item, error object otherwise.
  297. */
  298. public function get_item_permissions_check( $request ) {
  299. $comment = $this->get_comment( $request['id'] );
  300. if ( is_wp_error( $comment ) ) {
  301. return $comment;
  302. }
  303. if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
  304. return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit comments.' ), array( 'status' => rest_authorization_required_code() ) );
  305. }
  306. $post = get_post( $comment->comment_post_ID );
  307. if ( ! $this->check_read_permission( $comment, $request ) ) {
  308. return new WP_Error( 'rest_cannot_read', __( 'Sorry, you are not allowed to read this comment.' ), array( 'status' => rest_authorization_required_code() ) );
  309. }
  310. if ( $post && ! $this->check_read_post_permission( $post, $request ) ) {
  311. return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you are not allowed to read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) );
  312. }
  313. return true;
  314. }
  315. /**
  316. * Retrieves a comment.
  317. *
  318. * @since 4.7.0
  319. * @access public
  320. *
  321. * @param WP_REST_Request $request Full details about the request.
  322. * @return WP_Error|WP_REST_Response Response object on success, or error object on failure.
  323. */
  324. public function get_item( $request ) {
  325. $comment = $this->get_comment( $request['id'] );
  326. if ( is_wp_error( $comment ) ) {
  327. return $comment;
  328. }
  329. $data = $this->prepare_item_for_response( $comment, $request );
  330. $response = rest_ensure_response( $data );
  331. return $response;
  332. }
  333. /**
  334. * Checks if a given request has access to create a comment.
  335. *
  336. * @since 4.7.0
  337. * @access public
  338. *
  339. * @param WP_REST_Request $request Full details about the request.
  340. * @return WP_Error|bool True if the request has access to create items, error object otherwise.
  341. */
  342. public function create_item_permissions_check( $request ) {
  343. if ( ! is_user_logged_in() ) {
  344. if ( get_option( 'comment_registration' ) ) {
  345. return new WP_Error( 'rest_comment_login_required', __( 'Sorry, you must be logged in to comment.' ), array( 'status' => 401 ) );
  346. }
  347. /**
  348. * Filter whether comments can be created without authentication.
  349. *
  350. * Enables creating comments for anonymous users.
  351. *
  352. * @since 4.7.0
  353. *
  354. * @param bool $allow_anonymous Whether to allow anonymous comments to
  355. * be created. Default `false`.
  356. * @param WP_REST_Request $request Request used to generate the
  357. * response.
  358. */
  359. $allow_anonymous = apply_filters( 'rest_allow_anonymous_comments', false, $request );
  360. if ( ! $allow_anonymous ) {
  361. return new WP_Error( 'rest_comment_login_required', __( 'Sorry, you must be logged in to comment.' ), array( 'status' => 401 ) );
  362. }
  363. }
  364. // Limit who can set comment `author`, `author_ip` or `status` to anything other than the default.
  365. if ( isset( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( 'moderate_comments' ) ) {
  366. return new WP_Error( 'rest_comment_invalid_author',
  367. /* translators: %s: request parameter */
  368. sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'author' ),
  369. array( 'status' => rest_authorization_required_code() )
  370. );
  371. }
  372. if ( isset( $request['author_ip'] ) && ! current_user_can( 'moderate_comments' ) ) {
  373. if ( empty( $_SERVER['REMOTE_ADDR'] ) || $request['author_ip'] !== $_SERVER['REMOTE_ADDR'] ) {
  374. return new WP_Error( 'rest_comment_invalid_author_ip',
  375. /* translators: %s: request parameter */
  376. sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'author_ip' ),
  377. array( 'status' => rest_authorization_required_code() )
  378. );
  379. }
  380. }
  381. if ( isset( $request['status'] ) && ! current_user_can( 'moderate_comments' ) ) {
  382. return new WP_Error( 'rest_comment_invalid_status',
  383. /* translators: %s: request parameter */
  384. sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'status' ),
  385. array( 'status' => rest_authorization_required_code() )
  386. );
  387. }
  388. if ( empty( $request['post'] ) ) {
  389. return new WP_Error( 'rest_comment_invalid_post_id', __( 'Sorry, you are not allowed to create this comment without a post.' ), array( 'status' => 403 ) );
  390. }
  391. $post = get_post( (int) $request['post'] );
  392. if ( ! $post ) {
  393. return new WP_Error( 'rest_comment_invalid_post_id', __( 'Sorry, you are not allowed to create this comment without a post.' ), array( 'status' => 403 ) );
  394. }
  395. if ( 'draft' === $post->post_status ) {
  396. return new WP_Error( 'rest_comment_draft_post', __( 'Sorry, you are not allowed to create a comment on this post.' ), array( 'status' => 403 ) );
  397. }
  398. if ( 'trash' === $post->post_status ) {
  399. return new WP_Error( 'rest_comment_trash_post', __( 'Sorry, you are not allowed to create a comment on this post.' ), array( 'status' => 403 ) );
  400. }
  401. if ( ! $this->check_read_post_permission( $post, $request ) ) {
  402. return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you are not allowed to read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) );
  403. }
  404. if ( ! comments_open( $post->ID ) ) {
  405. return new WP_Error( 'rest_comment_closed', __( 'Sorry, comments are closed for this item.' ), array( 'status' => 403 ) );
  406. }
  407. return true;
  408. }
  409. /**
  410. * Creates a comment.
  411. *
  412. * @since 4.7.0
  413. * @access public
  414. *
  415. * @param WP_REST_Request $request Full details about the request.
  416. * @return WP_Error|WP_REST_Response Response object on success, or error object on failure.
  417. */
  418. public function create_item( $request ) {
  419. if ( ! empty( $request['id'] ) ) {
  420. return new WP_Error( 'rest_comment_exists', __( 'Cannot create existing comment.' ), array( 'status' => 400 ) );
  421. }
  422. // Do not allow comments to be created with a non-default type.
  423. if ( ! empty( $request['type'] ) && 'comment' !== $request['type'] ) {
  424. return new WP_Error( 'rest_invalid_comment_type', __( 'Cannot create a comment with that type.' ), array( 'status' => 400 ) );
  425. }
  426. $prepared_comment = $this->prepare_item_for_database( $request );
  427. if ( is_wp_error( $prepared_comment ) ) {
  428. return $prepared_comment;
  429. }
  430. $prepared_comment['comment_type'] = '';
  431. /*
  432. * Do not allow a comment to be created with missing or empty
  433. * comment_content. See wp_handle_comment_submission().
  434. */
  435. if ( empty( $prepared_comment['comment_content'] ) ) {
  436. return new WP_Error( 'rest_comment_content_invalid', __( 'Invalid comment content.' ), array( 'status' => 400 ) );
  437. }
  438. // Setting remaining values before wp_insert_comment so we can use wp_allow_comment().
  439. if ( ! isset( $prepared_comment['comment_date_gmt'] ) ) {
  440. $prepared_comment['comment_date_gmt'] = current_time( 'mysql', true );
  441. }
  442. // Set author data if the user's logged in.
  443. $missing_author = empty( $prepared_comment['user_id'] )
  444. && empty( $prepared_comment['comment_author'] )
  445. && empty( $prepared_comment['comment_author_email'] )
  446. && empty( $prepared_comment['comment_author_url'] );
  447. if ( is_user_logged_in() && $missing_author ) {
  448. $user = wp_get_current_user();
  449. $prepared_comment['user_id'] = $user->ID;
  450. $prepared_comment['comment_author'] = $user->display_name;
  451. $prepared_comment['comment_author_email'] = $user->user_email;
  452. $prepared_comment['comment_author_url'] = $user->user_url;
  453. }
  454. // Honor the discussion setting that requires a name and email address of the comment author.
  455. if ( get_option( 'require_name_email' ) ) {
  456. if ( empty( $prepared_comment['comment_author'] ) || empty( $prepared_comment['comment_author_email'] ) ) {
  457. return new WP_Error( 'rest_comment_author_data_required', __( 'Creating a comment requires valid author name and email values.' ), array( 'status' => 400 ) );
  458. }
  459. }
  460. if ( ! isset( $prepared_comment['comment_author_email'] ) ) {
  461. $prepared_comment['comment_author_email'] = '';
  462. }
  463. if ( ! isset( $prepared_comment['comment_author_url'] ) ) {
  464. $prepared_comment['comment_author_url'] = '';
  465. }
  466. if ( ! isset( $prepared_comment['comment_agent'] ) ) {
  467. $prepared_comment['comment_agent'] = '';
  468. }
  469. $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_comment );
  470. if ( is_wp_error( $check_comment_lengths ) ) {
  471. $error_code = $check_comment_lengths->get_error_code();
  472. return new WP_Error( $error_code, __( 'Comment field exceeds maximum length allowed.' ), array( 'status' => 400 ) );
  473. }
  474. $prepared_comment['comment_approved'] = wp_allow_comment( $prepared_comment, true );
  475. if ( is_wp_error( $prepared_comment['comment_approved'] ) ) {
  476. $error_code = $prepared_comment['comment_approved']->get_error_code();
  477. $error_message = $prepared_comment['comment_approved']->get_error_message();
  478. if ( 'comment_duplicate' === $error_code ) {
  479. return new WP_Error( $error_code, $error_message, array( 'status' => 409 ) );
  480. }
  481. if ( 'comment_flood' === $error_code ) {
  482. return new WP_Error( $error_code, $error_message, array( 'status' => 400 ) );
  483. }
  484. return $prepared_comment['comment_approved'];
  485. }
  486. /**
  487. * Filters a comment before it is inserted via the REST API.
  488. *
  489. * Allows modification of the comment right before it is inserted via wp_insert_comment().
  490. *
  491. * @since 4.7.0
  492. *
  493. * @param array $prepared_comment The prepared comment data for wp_insert_comment().
  494. * @param WP_REST_Request $request Request used to insert the comment.
  495. */
  496. $prepared_comment = apply_filters( 'rest_pre_insert_comment', $prepared_comment, $request );
  497. $comment_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_comment ) ) );
  498. if ( ! $comment_id ) {
  499. return new WP_Error( 'rest_comment_failed_create', __( 'Creating comment failed.' ), array( 'status' => 500 ) );
  500. }
  501. if ( isset( $request['status'] ) ) {
  502. $this->handle_status_param( $request['status'], $comment_id );
  503. }
  504. $comment = get_comment( $comment_id );
  505. /**
  506. * Fires after a comment is created or updated via the REST API.
  507. *
  508. * @since 4.7.0
  509. *
  510. * @param WP_Comment $comment Inserted or updated comment object.
  511. * @param WP_REST_Request $request Request object.
  512. * @param bool $creating True when creating a comment, false
  513. * when updating.
  514. */
  515. do_action( 'rest_insert_comment', $comment, $request, true );
  516. $schema = $this->get_item_schema();
  517. if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
  518. $meta_update = $this->meta->update_value( $request['meta'], $comment_id );
  519. if ( is_wp_error( $meta_update ) ) {
  520. return $meta_update;
  521. }
  522. }
  523. $fields_update = $this->update_additional_fields_for_object( $comment, $request );
  524. if ( is_wp_error( $fields_update ) ) {
  525. return $fields_update;
  526. }
  527. $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
  528. $request->set_param( 'context', $context );
  529. $response = $this->prepare_item_for_response( $comment, $request );
  530. $response = rest_ensure_response( $response );
  531. $response->set_status( 201 );
  532. $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment_id ) ) );
  533. return $response;
  534. }
  535. /**
  536. * Checks if a given REST request has access to update a comment.
  537. *
  538. * @since 4.7.0
  539. * @access public
  540. *
  541. * @param WP_REST_Request $request Full details about the request.
  542. * @return WP_Error|bool True if the request has access to update the item, error object otherwise.
  543. */
  544. public function update_item_permissions_check( $request ) {
  545. $comment = $this->get_comment( $request['id'] );
  546. if ( is_wp_error( $comment ) ) {
  547. return $comment;
  548. }
  549. if ( ! $this->check_edit_permission( $comment ) ) {
  550. return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this comment.' ), array( 'status' => rest_authorization_required_code() ) );
  551. }
  552. return true;
  553. }
  554. /**
  555. * Updates a comment.
  556. *
  557. * @since 4.7.0
  558. * @access public
  559. *
  560. * @param WP_REST_Request $request Full details about the request.
  561. * @return WP_Error|WP_REST_Response Response object on success, or error object on failure.
  562. */
  563. public function update_item( $request ) {
  564. $comment = $this->get_comment( $request['id'] );
  565. if ( is_wp_error( $comment ) ) {
  566. return $comment;
  567. }
  568. $id = $comment->comment_ID;
  569. if ( isset( $request['type'] ) && get_comment_type( $id ) !== $request['type'] ) {
  570. return new WP_Error( 'rest_comment_invalid_type', __( 'Sorry, you are not allowed to change the comment type.' ), array( 'status' => 404 ) );
  571. }
  572. $prepared_args = $this->prepare_item_for_database( $request );
  573. if ( is_wp_error( $prepared_args ) ) {
  574. return $prepared_args;
  575. }
  576. if ( ! empty( $prepared_args['comment_post_ID'] ) ) {
  577. $post = get_post( $prepared_args['comment_post_ID'] );
  578. if ( empty( $post ) ) {
  579. return new WP_Error( 'rest_comment_invalid_post_id', __( 'Invalid post ID.' ), array( 'status' => 403 ) );
  580. }
  581. }
  582. if ( empty( $prepared_args ) && isset( $request['status'] ) ) {
  583. // Only the comment status is being changed.
  584. $change = $this->handle_status_param( $request['status'], $id );
  585. if ( ! $change ) {
  586. return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment status failed.' ), array( 'status' => 500 ) );
  587. }
  588. } elseif ( ! empty( $prepared_args ) ) {
  589. if ( is_wp_error( $prepared_args ) ) {
  590. return $prepared_args;
  591. }
  592. if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) {
  593. return new WP_Error( 'rest_comment_content_invalid', __( 'Invalid comment content.' ), array( 'status' => 400 ) );
  594. }
  595. $prepared_args['comment_ID'] = $id;
  596. $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args );
  597. if ( is_wp_error( $check_comment_lengths ) ) {
  598. $error_code = $check_comment_lengths->get_error_code();
  599. return new WP_Error( $error_code, __( 'Comment field exceeds maximum length allowed.' ), array( 'status' => 400 ) );
  600. }
  601. $updated = wp_update_comment( wp_slash( (array) $prepared_args ) );
  602. if ( false === $updated ) {
  603. return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment failed.' ), array( 'status' => 500 ) );
  604. }
  605. if ( isset( $request['status'] ) ) {
  606. $this->handle_status_param( $request['status'], $id );
  607. }
  608. }
  609. $comment = get_comment( $id );
  610. /* This action is documented in lib/endpoints/class-wp-rest-comments-controller.php */
  611. do_action( 'rest_insert_comment', $comment, $request, false );
  612. $schema = $this->get_item_schema();
  613. if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
  614. $meta_update = $this->meta->update_value( $request['meta'], $id );
  615. if ( is_wp_error( $meta_update ) ) {
  616. return $meta_update;
  617. }
  618. }
  619. $fields_update = $this->update_additional_fields_for_object( $comment, $request );
  620. if ( is_wp_error( $fields_update ) ) {
  621. return $fields_update;
  622. }
  623. $request->set_param( 'context', 'edit' );
  624. $response = $this->prepare_item_for_response( $comment, $request );
  625. return rest_ensure_response( $response );
  626. }
  627. /**
  628. * Checks if a given request has access to delete a comment.
  629. *
  630. * @since 4.7.0
  631. * @access public
  632. *
  633. * @param WP_REST_Request $request Full details about the request.
  634. * @return WP_Error|bool True if the request has access to delete the item, error object otherwise.
  635. */
  636. public function delete_item_permissions_check( $request ) {
  637. $comment = $this->get_comment( $request['id'] );
  638. if ( is_wp_error( $comment ) ) {
  639. return $comment;
  640. }
  641. if ( ! $this->check_edit_permission( $comment ) ) {
  642. return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete this comment.' ), array( 'status' => rest_authorization_required_code() ) );
  643. }
  644. return true;
  645. }
  646. /**
  647. * Deletes a comment.
  648. *
  649. * @since 4.7.0
  650. * @access public
  651. *
  652. * @param WP_REST_Request $request Full details about the request.
  653. * @return WP_Error|WP_REST_Response Response object on success, or error object on failure.
  654. */
  655. public function delete_item( $request ) {
  656. $comment = $this->get_comment( $request['id'] );
  657. if ( is_wp_error( $comment ) ) {
  658. return $comment;
  659. }
  660. $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
  661. /**
  662. * Filters whether a comment can be trashed.
  663. *
  664. * Return false to disable trash support for the post.
  665. *
  666. * @since 4.7.0
  667. *
  668. * @param bool $supports_trash Whether the post type support trashing.
  669. * @param WP_Post $comment The comment object being considered for trashing support.
  670. */
  671. $supports_trash = apply_filters( 'rest_comment_trashable', ( EMPTY_TRASH_DAYS > 0 ), $comment );
  672. $request->set_param( 'context', 'edit' );
  673. if ( $force ) {
  674. $previous = $this->prepare_item_for_response( $comment, $request );
  675. $result = wp_delete_comment( $comment->comment_ID, true );
  676. $response = new WP_REST_Response();
  677. $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data() ) );
  678. } else {
  679. // If this type doesn't support trashing, error out.
  680. if ( ! $supports_trash ) {
  681. return new WP_Error( 'rest_trash_not_supported', __( 'The comment does not support trashing. Set force=true to delete.' ), array( 'status' => 501 ) );
  682. }
  683. if ( 'trash' === $comment->comment_approved ) {
  684. return new WP_Error( 'rest_already_trashed', __( 'The comment has already been trashed.' ), array( 'status' => 410 ) );
  685. }
  686. $result = wp_trash_comment( $comment->comment_ID );
  687. $comment = get_comment( $comment->comment_ID );
  688. $response = $this->prepare_item_for_response( $comment, $request );
  689. }
  690. if ( ! $result ) {
  691. return new WP_Error( 'rest_cannot_delete', __( 'The comment cannot be deleted.' ), array( 'status' => 500 ) );
  692. }
  693. /**
  694. * Fires after a comment is deleted via the REST API.
  695. *
  696. * @since 4.7.0
  697. *
  698. * @param WP_Comment $comment The deleted comment data.
  699. * @param WP_REST_Response $response The response returned from the API.
  700. * @param WP_REST_Request $request The request sent to the API.
  701. */
  702. do_action( 'rest_delete_comment', $comment, $response, $request );
  703. return $response;
  704. }
  705. /**
  706. * Prepares a single comment output for response.
  707. *
  708. * @since 4.7.0
  709. * @access public
  710. *
  711. * @param WP_Comment $comment Comment object.
  712. * @param WP_REST_Request $request Request object.
  713. * @return WP_REST_Response Response object.
  714. */
  715. public function prepare_item_for_response( $comment, $request ) {
  716. $data = array(
  717. 'id' => (int) $comment->comment_ID,
  718. 'post' => (int) $comment->comment_post_ID,
  719. 'parent' => (int) $comment->comment_parent,
  720. 'author' => (int) $comment->user_id,
  721. 'author_name' => $comment->comment_author,
  722. 'author_email' => $comment->comment_author_email,
  723. 'author_url' => $comment->comment_author_url,
  724. 'author_ip' => $comment->comment_author_IP,
  725. 'author_user_agent' => $comment->comment_agent,
  726. 'date' => mysql_to_rfc3339( $comment->comment_date ),
  727. 'date_gmt' => mysql_to_rfc3339( $comment->comment_date_gmt ),
  728. 'content' => array(
  729. /** This filter is documented in wp-includes/comment-template.php */
  730. 'rendered' => apply_filters( 'comment_text', $comment->comment_content, $comment ),
  731. 'raw' => $comment->comment_content,
  732. ),
  733. 'link' => get_comment_link( $comment ),
  734. 'status' => $this->prepare_status_response( $comment->comment_approved ),
  735. 'type' => get_comment_type( $comment->comment_ID ),
  736. );
  737. $schema = $this->get_item_schema();
  738. if ( ! empty( $schema['properties']['author_avatar_urls'] ) ) {
  739. $data['author_avatar_urls'] = rest_get_avatar_urls( $comment->comment_author_email );
  740. }
  741. if ( ! empty( $schema['properties']['meta'] ) ) {
  742. $data['meta'] = $this->meta->get_value( $comment->comment_ID, $request );
  743. }
  744. $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
  745. $data = $this->add_additional_fields_to_object( $data, $request );
  746. $data = $this->filter_response_by_context( $data, $context );
  747. // Wrap the data in a response object.
  748. $response = rest_ensure_response( $data );
  749. $response->add_links( $this->prepare_links( $comment ) );
  750. /**
  751. * Filters a comment returned from the API.
  752. *
  753. * Allows modification of the comment right before it is returned.
  754. *
  755. * @since 4.7.0
  756. *
  757. * @param WP_REST_Response $response The response object.
  758. * @param WP_Comment $comment The original comment object.
  759. * @param WP_REST_Request $request Request used to generate the response.
  760. */
  761. return apply_filters( 'rest_prepare_comment', $response, $comment, $request );
  762. }
  763. /**
  764. * Prepares links for the request.
  765. *
  766. * @since 4.7.0
  767. * @access protected
  768. *
  769. * @param WP_Comment $comment Comment object.
  770. * @return array Links for the given comment.
  771. */
  772. protected function prepare_links( $comment ) {
  773. $links = array(
  774. 'self' => array(
  775. 'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_ID ) ),
  776. ),
  777. 'collection' => array(
  778. 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
  779. ),
  780. );
  781. if ( 0 !== (int) $comment->user_id ) {
  782. $links['author'] = array(
  783. 'href' => rest_url( 'wp/v2/users/' . $comment->user_id ),
  784. 'embeddable' => true,
  785. );
  786. }
  787. if ( 0 !== (int) $comment->comment_post_ID ) {
  788. $post = get_post( $comment->comment_post_ID );
  789. if ( ! empty( $post->ID ) ) {
  790. $obj = get_post_type_object( $post->post_type );
  791. $base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
  792. $links['up'] = array(
  793. 'href' => rest_url( 'wp/v2/' . $base . '/' . $comment->comment_post_ID ),
  794. 'embeddable' => true,
  795. 'post_type' => $post->post_type,
  796. );
  797. }
  798. }
  799. if ( 0 !== (int) $comment->comment_parent ) {
  800. $links['in-reply-to'] = array(
  801. 'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_parent ) ),
  802. 'embeddable' => true,
  803. );
  804. }
  805. // Only grab one comment to verify the comment has children.
  806. $comment_children = $comment->get_children( array(
  807. 'number' => 1,
  808. 'count' => true
  809. ) );
  810. if ( ! empty( $comment_children ) ) {
  811. $args = array(
  812. 'parent' => $comment->comment_ID
  813. );
  814. $rest_url = add_query_arg( $args, rest_url( $this->namespace . '/' . $this->rest_base ) );
  815. $links['children'] = array(
  816. 'href' => $rest_url,
  817. );
  818. }
  819. return $links;
  820. }
  821. /**
  822. * Prepends internal property prefix to query parameters to match our response fields.
  823. *
  824. * @since 4.7.0
  825. * @access protected
  826. *
  827. * @param string $query_param Query parameter.
  828. * @return string The normalized query parameter.
  829. */
  830. protected function normalize_query_param( $query_param ) {
  831. $prefix = 'comment_';
  832. switch ( $query_param ) {
  833. case 'id':
  834. $normalized = $prefix . 'ID';
  835. break;
  836. case 'post':
  837. $normalized = $prefix . 'post_ID';
  838. break;
  839. case 'parent':
  840. $normalized = $prefix . 'parent';
  841. break;
  842. case 'include':
  843. $normalized = 'comment__in';
  844. break;
  845. default:
  846. $normalized = $prefix . $query_param;
  847. break;
  848. }
  849. return $normalized;
  850. }
  851. /**
  852. * Checks comment_approved to set comment status for single comment output.
  853. *
  854. * @since 4.7.0
  855. * @access protected
  856. *
  857. * @param string|int $comment_approved comment status.
  858. * @return string Comment status.
  859. */
  860. protected function prepare_status_response( $comment_approved ) {
  861. switch ( $comment_approved ) {
  862. case 'hold':
  863. case '0':
  864. $status = 'hold';
  865. break;
  866. case 'approve':
  867. case '1':
  868. $status = 'approved';
  869. break;
  870. case 'spam':
  871. case 'trash':
  872. default:
  873. $status = $comment_approved;
  874. break;
  875. }
  876. return $status;
  877. }
  878. /**
  879. * Prepares a single comment to be inserted into the database.
  880. *
  881. * @since 4.7.0
  882. * @access protected
  883. *
  884. * @param WP_REST_Request $request Request object.
  885. * @return array|WP_Error Prepared comment, otherwise WP_Error object.
  886. */
  887. protected function prepare_item_for_database( $request ) {
  888. $prepared_comment = array();
  889. /*
  890. * Allow the comment_content to be set via the 'content' or
  891. * the 'content.raw' properties of the Request object.
  892. */
  893. if ( isset( $request['content'] ) && is_string( $request['content'] ) ) {
  894. $prepared_comment['comment_content'] = $request['content'];
  895. } elseif ( isset( $request['content']['raw'] ) && is_string( $request['content']['raw'] ) ) {
  896. $prepared_comment['comment_content'] = $request['content']['raw'];
  897. }
  898. if ( isset( $request['post'] ) ) {
  899. $prepared_comment['comment_post_ID'] = (int) $request['post'];
  900. }
  901. if ( isset( $request['parent'] ) ) {
  902. $prepared_comment['comment_parent'] = $request['parent'];
  903. }
  904. if ( isset( $request['author'] ) ) {
  905. $user = new WP_User( $request['author'] );
  906. if ( $user->exists() ) {
  907. $prepared_comment['user_id'] = $user->ID;
  908. $prepared_comment['comment_author'] = $user->display_name;
  909. $prepared_comment['comment_author_email'] = $user->user_email;
  910. $prepared_comment['comment_author_url'] = $user->user_url;
  911. } else {
  912. return new WP_Error( 'rest_comment_author_invalid', __( 'Invalid comment author ID.' ), array( 'status' => 400 ) );
  913. }
  914. }
  915. if ( isset( $request['author_name'] ) ) {
  916. $prepared_comment['comment_author'] = $request['author_name'];
  917. }
  918. if ( isset( $request['author_email'] ) ) {
  919. $prepared_comment['comment_author_email'] = $request['author_email'];
  920. }
  921. if ( isset( $request['author_url'] ) ) {
  922. $prepared_comment['comment_author_url'] = $request['author_url'];
  923. }
  924. if ( isset( $request['author_ip'] ) && current_user_can( 'moderate_comments' ) ) {
  925. $prepared_comment['comment_author_IP'] = $request['author_ip'];
  926. } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( $_SERVER['REMOTE_ADDR'] ) ) {
  927. $prepared_comment['comment_author_IP'] = $_SERVER['REMOTE_ADDR'];
  928. } else {
  929. $prepared_comment['comment_author_IP'] = '127.0.0.1';
  930. }
  931. if ( ! empty( $request['author_user_agent'] ) ) {
  932. $prepared_comment['comment_agent'] = $request['author_user_agent'];
  933. } elseif ( $request->get_header( 'user_agent' ) ) {
  934. $prepared_comment['comment_agent'] = $request->get_header( 'user_agent' );
  935. }
  936. if ( ! empty( $request['date'] ) ) {
  937. $date_data = rest_get_date_with_gmt( $request['date'] );
  938. if ( ! empty( $date_data ) ) {
  939. list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
  940. }
  941. } elseif ( ! empty( $request['date_gmt'] ) ) {
  942. $date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
  943. if ( ! empty( $date_data ) ) {
  944. list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
  945. }
  946. }
  947. /**
  948. * Filters a comment after it is prepared for the database.
  949. *
  950. * Allows modification of the comment right after it is prepared for the database.
  951. *
  952. * @since 4.7.0
  953. *
  954. * @param array $prepared_comment The prepared comment data for `wp_insert_comment`.
  955. * @param WP_REST_Request $request The current request.
  956. */
  957. return apply_filters( 'rest_preprocess_comment', $prepared_comment, $request );
  958. }
  959. /**
  960. * Retrieves the comment's schema, conforming to JSON Schema.
  961. *
  962. * @since 4.7.0
  963. * @access public
  964. *
  965. * @return array
  966. */
  967. public function get_item_schema() {
  968. $schema = array(
  969. '$schema' => 'http://json-schema.org/schema#',
  970. 'title' => 'comment',
  971. 'type' => 'object',
  972. 'properties' => array(
  973. 'id' => array(
  974. 'description' => __( 'Unique identifier for the object.' ),
  975. 'type' => 'integer',
  976. 'context' => array( 'view', 'edit', 'embed' ),
  977. 'readonly' => true,
  978. ),
  979. 'author' => array(
  980. 'description' => __( 'The ID of the user object, if author was a user.' ),
  981. 'type' => 'integer',
  982. 'context' => array( 'view', 'edit', 'embed' ),
  983. ),
  984. 'author_email' => array(
  985. 'description' => __( 'Email address for the object author.' ),
  986. 'type' => 'string',
  987. 'format' => 'email',
  988. 'context' => array( 'edit' ),
  989. 'arg_options' => array(
  990. 'sanitize_callback' => array( $this, 'check_comment_author_email' ),
  991. 'validate_callback' => null, // skip built-in validation of 'email'.
  992. ),
  993. ),
  994. 'author_ip' => array(
  995. 'description' => __( 'IP address for the object author.' ),
  996. 'type' => 'string',
  997. 'format' => 'ip',
  998. 'context' => array( 'edit' ),
  999. ),
  1000. 'author_name' => array(
  1001. 'description' => __( 'Display name for the object author.' ),
  1002. 'type' => 'string',
  1003. 'context' => array( 'view', 'edit', 'embed' ),
  1004. 'arg_options' => array(
  1005. 'sanitize_callback' => 'sanitize_text_field',
  1006. ),
  1007. ),
  1008. 'author_url' => array(
  1009. 'description' => __( 'URL for the object author.' ),
  1010. 'type' => 'string',
  1011. 'format' => 'uri',
  1012. 'context' => array( 'view', 'edit', 'embed' ),
  1013. ),
  1014. 'author_user_agent' => array(
  1015. 'description' => __( 'User agent for the object author.' ),
  1016. 'type' => 'string',
  1017. 'context' => array( 'edit' ),
  1018. 'arg_options' => array(
  1019. 'sanitize_callback' => 'sanitize_text_field',
  1020. ),
  1021. ),
  1022. 'content' => array(
  1023. 'description' => __( 'The content for the object.' ),
  1024. 'type' => 'object',
  1025. 'context' => array( 'view', 'edit', 'embed' ),
  1026. 'arg_options' => array(
  1027. 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database()
  1028. ),
  1029. 'properties' => array(
  1030. 'raw' => array(
  1031. 'description' => __( 'Content for the object, as it exists in the database.' ),
  1032. 'type' => 'string',
  1033. 'context' => array( 'edit' ),
  1034. ),
  1035. 'rendered' => array(
  1036. 'description' => __( 'HTML content for the object, transformed for display.' ),
  1037. 'type' => 'string',
  1038. 'context' => array( 'view', 'edit', 'embed' ),
  1039. 'readonly' => true,
  1040. ),
  1041. ),
  1042. ),
  1043. 'date' => array(
  1044. 'description' => __( "The date the object was published, in the site's timezone." ),
  1045. 'type' => 'string',
  1046. 'format' => 'date-time',
  1047. 'context' => array( 'view', 'edit', 'embed' ),
  1048. ),
  1049. 'date_gmt' => array(
  1050. 'description' => __( 'The date the object was published, as GMT.' ),
  1051. 'type' => 'string',
  1052. 'format' => 'date-time',
  1053. 'context' => array( 'view', 'edit' ),
  1054. ),
  1055. 'link' => array(
  1056. 'description' => __( 'URL to the object.' ),
  1057. 'type' => 'string',
  1058. 'format' => 'uri',
  1059. 'context' => array( 'view', 'edit', 'embed' ),
  1060. 'readonly' => true,
  1061. ),
  1062. 'parent' => array(
  1063. 'description' => __( 'The ID for the parent of the object.' ),
  1064. 'type' => 'integer',
  1065. 'context' => array( 'view', 'edit', 'embed' ),
  1066. 'default' => 0,
  1067. ),
  1068. 'post' => array(
  1069. 'description' => __( 'The ID of the associated post object.' ),
  1070. 'type' => 'integer',
  1071. 'context' => array( 'view', 'edit' ),
  1072. 'default' => 0,
  1073. ),
  1074. 'status' => array(
  1075. 'description' => __( 'State of the object.' ),
  1076. 'type' => 'string',
  1077. 'context' => array( 'view', 'edit' ),
  1078. 'arg_options' => array(
  1079. 'sanitize_callback' => 'sanitize_key',
  1080. ),
  1081. ),
  1082. 'type' => array(
  1083. 'description' => __( 'Type of Comment for the object.' ),
  1084. 'type' => 'string',
  1085. 'context' => array( 'view', 'edit', 'embed' ),
  1086. 'readonly' => true,
  1087. ),
  1088. ),
  1089. );
  1090. if ( get_option( 'show_avatars' ) ) {
  1091. $avatar_properties = array();
  1092. $avatar_sizes = rest_get_avatar_sizes();
  1093. foreach ( $avatar_sizes as $size ) {
  1094. $avatar_properties[ $size ] = array(
  1095. /* translators: %d: avatar image size in pixels */
  1096. 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
  1097. 'type' => 'string',
  1098. 'format' => 'uri',
  1099. 'context' => array( 'embed', 'view', 'edit' ),
  1100. );
  1101. }
  1102. $schema['properties']['author_avatar_urls'] = array(
  1103. 'description' => __( 'Avatar URLs for the object author.' ),
  1104. 'type' => 'object',
  1105. 'context' => array( 'view', 'edit', 'embed' ),
  1106. 'readonly' => true,
  1107. 'properties' => $avatar_properties,
  1108. );
  1109. }
  1110. $schema['properties']['meta'] = $this->meta->get_field_schema();
  1111. return $this->add_additional_fields_schema( $schema );
  1112. }
  1113. /**
  1114. * Retrieves the query params for collections.
  1115. *
  1116. * @since 4.7.0
  1117. * @access public
  1118. *
  1119. * @return array Comments collection parameters.
  1120. */
  1121. public function get_collection_params() {
  1122. $query_params = parent::get_collection_params();
  1123. $query_params['context']['default'] = 'view';
  1124. $query_params['after'] = array(
  1125. 'description' => __( 'Limit response to comments published after a given ISO8601 compliant date.' ),
  1126. 'type' => 'string',
  1127. 'format' => 'date-time',
  1128. );
  1129. $query_params['author'] = array(
  1130. 'description' => __( 'Limit result set to comments assigned to specific user IDs. Requires authorization.' ),
  1131. 'type' => 'array',
  1132. 'items' => array(
  1133. 'type' => 'integer',
  1134. ),
  1135. );
  1136. $query_params['author_exclude'] = array(
  1137. 'description' => __( 'Ensure result set excludes comments assigned to specific user IDs. Requires authorization.' ),
  1138. 'type' => 'array',
  1139. 'items' => array(
  1140. 'type' => 'integer',
  1141. ),
  1142. );
  1143. $query_params['author_email'] = array(
  1144. 'default' => null,
  1145. 'description' => __( 'Limit result set to that from a specific author email. Requires authorization.' ),
  1146. 'format' => 'email',
  1147. 'type' => 'string',
  1148. );
  1149. $query_params['before'] = array(
  1150. 'description' => __( 'Limit response to comments published before a given ISO8601 compliant date.' ),
  1151. 'type' => 'string',
  1152. 'format' => 'date-time',
  1153. );
  1154. $query_params['exclude'] = array(
  1155. 'description' => __( 'Ensure result set excludes specific IDs.' ),
  1156. 'type' => 'array',
  1157. 'items' => array(
  1158. 'type' => 'integer',
  1159. ),
  1160. 'default' => array(),
  1161. );
  1162. $query_params['include'] = array(
  1163. 'description' => __( 'Limit result set to specific IDs.' ),
  1164. 'type' => 'array',
  1165. 'items' => array(
  1166. 'type' => 'integer',
  1167. ),
  1168. 'default' => array(),
  1169. );
  1170. $query_params['offset'] = array(
  1171. 'description' => __( 'Offset the result set by a specific number of items.' ),
  1172. 'type' => 'integer',
  1173. );
  1174. $query_params['order'] = array(
  1175. 'description' => __( 'Order sort attribute ascending or descending.' ),
  1176. 'type' => 'string',
  1177. 'default' => 'desc',
  1178. 'enum' => array(
  1179. 'asc',
  1180. 'desc',
  1181. ),
  1182. );
  1183. $query_params['orderby'] = array(
  1184. 'description' => __( 'Sort collection by object attribute.' ),
  1185. 'type' => 'string',
  1186. 'default' => 'date_gmt',
  1187. 'enum' => array(
  1188. 'date',
  1189. 'date_gmt',
  1190. 'id',
  1191. 'include',
  1192. 'post',
  1193. 'parent',
  1194. 'type',
  1195. ),
  1196. );
  1197. $query_params['parent'] = array(
  1198. 'default' => array(),
  1199. 'description' => __( 'Limit result set to comments of specific parent IDs.' ),
  1200. 'type' => 'array',
  1201. 'items' => array(
  1202. 'type' => 'integer',
  1203. ),
  1204. );
  1205. $query_params['parent_exclude'] = array(
  1206. 'default' => array(),
  1207. 'description' => __( 'Ensure result set excludes specific parent IDs.' ),
  1208. 'type' => 'array',
  1209. 'items' => array(
  1210. 'type' => 'integer',
  1211. ),
  1212. );
  1213. $query_params['post'] = array(
  1214. 'default' => array(),
  1215. 'description' => __( 'Limit result set to comments assigned to specific post IDs.' ),
  1216. 'type' => 'array',
  1217. 'items' => array(
  1218. 'type' => 'integer',
  1219. ),
  1220. );
  1221. $query_params['status'] = array(
  1222. 'default' => 'approve',
  1223. 'description' => __( 'Limit result set to comments assigned a specific status. Requires authorization.' ),
  1224. 'sanitize_callback' => 'sanitize_key',
  1225. 'type' => 'string',
  1226. 'validate_callback' => 'rest_validate_request_arg',
  1227. );
  1228. $query_params['type'] = array(
  1229. 'default' => 'comment',
  1230. 'description' => __( 'Limit result set to comments assigned a specific type. Requires authorization.' ),
  1231. 'sanitize_callback' => 'sanitize_key',
  1232. 'type' => 'string',
  1233. 'validate_callback' => 'rest_validate_request_arg',
  1234. );
  1235. $query_params['password'] = array(
  1236. 'description' => __( 'The password for the post if it is password protected.' ),
  1237. 'type' => 'string',
  1238. );
  1239. /**
  1240. * Filter collection parameters for the comments controller.
  1241. *
  1242. * This filter registers the collection parameter, but does not map the
  1243. * collection parameter to an internal WP_Comment_Query parameter. Use the
  1244. * `rest_comment_query` filter to set WP_Comment_Query parameters.
  1245. *
  1246. * @since 4.7.0
  1247. *
  1248. * @param array $query_params JSON Schema-formatted collection parameters.
  1249. */
  1250. return apply_filters( 'rest_comment_collection_params', $query_params );
  1251. }
  1252. /**
  1253. * Sets the comment_status of a given comment object when creating or updating a comment.
  1254. *
  1255. * @since 4.7.0
  1256. * @access protected
  1257. *
  1258. * @param string|int $new_status New comment status.
  1259. * @param int $comment_id Comment ID.
  1260. * @return bool Whether the status was changed.
  1261. */
  1262. protected function handle_status_param( $new_status, $comment_id ) {
  1263. $old_status = wp_get_comment_status( $comment_id );
  1264. if ( $new_status === $old_status ) {
  1265. return false;
  1266. }
  1267. switch ( $new_status ) {
  1268. case 'approved' :
  1269. case 'approve':
  1270. case '1':
  1271. $changed = wp_set_comment_status( $comment_id, 'approve' );
  1272. break;
  1273. case 'hold':
  1274. case '0':
  1275. $changed = wp_set_comment_status( $comment_id, 'hold' );
  1276. break;
  1277. case 'spam' :
  1278. $changed = wp_spam_comment( $comment_id );
  1279. break;
  1280. case 'unspam' :
  1281. $changed = wp_unspam_comment( $comment_id );
  1282. break;
  1283. case 'trash' :
  1284. $changed = wp_trash_comment( $comment_id );
  1285. break;
  1286. case 'untrash' :
  1287. $changed = wp_untrash_comment( $comment_id );
  1288. break;
  1289. default :
  1290. $changed = false;
  1291. break;
  1292. }
  1293. return $changed;
  1294. }
  1295. /**
  1296. * Checks if the post can be read.
  1297. *
  1298. * Correctly handles posts with the inherit status.
  1299. *
  1300. * @since 4.7.0
  1301. * @access protected
  1302. *
  1303. * @param WP_Post $post Post object.
  1304. * @param WP_REST_Request $request Request data to check.
  1305. * @return bool Whether post can be read.
  1306. */
  1307. protected function check_read_post_permission( $post, $request ) {
  1308. $posts_controller = new WP_REST_Posts_Controller( $post->post_type );
  1309. $post_type = get_post_type_object( $post->post_type );
  1310. $has_password_filter = false;
  1311. // Only check password if a specific post was queried for or a single comment
  1312. $requested_post = ! empty( $request['post'] ) && 1 === count( $request['post'] );
  1313. $requested_comment = ! empty( $request['id'] );
  1314. if ( ( $requested_post || $requested_comment ) && $posts_controller->can_access_password_content( $post, $request ) ) {
  1315. add_filter( 'post_password_required', '__return_false' );
  1316. $has_password_filter = true;
  1317. }
  1318. if ( post_password_required( $post ) ) {
  1319. $result = current_user_can( $post_type->cap->edit_post, $post->ID );
  1320. } else {
  1321. $result = $posts_controller->check_read_permission( $post );
  1322. }
  1323. if ( $has_password_filter ) {
  1324. remove_filter( 'post_password_required', '__return_false' );
  1325. }
  1326. return $result;
  1327. }
  1328. /**
  1329. * Checks if the comment can be read.
  1330. *
  1331. * @since 4.7.0
  1332. * @access protected
  1333. *
  1334. * @param WP_Comment $comment Comment object.
  1335. * @param WP_REST_Request $request Request data to check.
  1336. * @return bool Whether the comment can be read.
  1337. */
  1338. protected function check_read_permission( $comment, $request ) {
  1339. if ( ! empty( $comment->comment_post_ID ) ) {
  1340. $post = get_post( $comment->comment_post_ID );
  1341. if ( $post ) {
  1342. if ( $this->check_read_post_permission( $post, $request ) && 1 === (int) $comment->comment_approved ) {
  1343. return true;
  1344. }
  1345. }
  1346. }
  1347. if ( 0 === get_current_user_id() ) {
  1348. return false;
  1349. }
  1350. if ( empty( $comment->comment_post_ID ) && ! current_user_can( 'moderate_comments' ) ) {
  1351. return false;
  1352. }
  1353. if ( ! empty( $comment->user_id ) && get_current_user_id() === (int) $comment->user_id ) {
  1354. return true;
  1355. }
  1356. return current_user_can( 'edit_comment', $comment->comment_ID );
  1357. }
  1358. /**
  1359. * Checks if a comment can be edited or deleted.
  1360. *
  1361. * @since 4.7.0
  1362. * @access protected
  1363. *
  1364. * @param object $comment Comment object.
  1365. * @return bool Whether the comment can be edited or deleted.
  1366. */
  1367. protected function check_edit_permission( $comment ) {
  1368. if ( 0 === (int) get_current_user_id() ) {
  1369. return false;
  1370. }
  1371. if ( ! current_user_can( 'moderate_comments' ) ) {
  1372. return false;
  1373. }
  1374. return current_user_can( 'edit_comment', $comment->comment_ID );
  1375. }
  1376. /**
  1377. * Checks a comment author email for validity.
  1378. *
  1379. * Accepts either a valid email address or empty string as a valid comment
  1380. * author email address. Setting the comment author email to an empty
  1381. * string is allowed when a comment is being updated.
  1382. *
  1383. * @since 4.7.0
  1384. *
  1385. * @param string $value Author email value submitted.
  1386. * @param WP_REST_Request $request Full details about the request.
  1387. * @param string $param The parameter name.
  1388. * @return WP_Error|string The sanitized email address, if valid,
  1389. * otherwise an error.
  1390. */
  1391. public function check_comment_author_email( $value, $request, $param ) {
  1392. $email = (string) $value;
  1393. if ( empty( $email ) ) {
  1394. return $email;
  1395. }
  1396. $check_email = rest_validate_request_arg( $email, $request, $param );
  1397. if ( is_wp_error( $check_email ) ) {
  1398. return $check_email;
  1399. }
  1400. return $email;
  1401. }
  1402. }