class-wp-rest-comments-controller.php 53 KB

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