WordPress REST API (Version 2) - Version 2.0-beta10

Version Description

Download this release

Release Info

Developer danielbachhuber
Plugin Icon 128x128 WordPress REST API (Version 2)
Version 2.0-beta10
Comparing to
See all releases

Code changes from version 2.0-beta3.1 to 2.0-beta10

CHANGELOG.md CHANGED
@@ -1,11 +1,683 @@
1
  # Changelog
2
 
3
- ## 2.0 Beta 3.1
4
 
5
- - Ensure media of private posts are private too.
6
 
7
  Reported by @danielbachhuber on 2016-01-08.
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  ## 2.0 Beta 3.0
10
 
11
  - Add ability to declare sanitization and default options for schema fields.
1
  # Changelog
2
 
3
+ ## 2.0 Beta 10.0
4
 
5
+ - SECURITY: Ensure media of private posts are private too.
6
 
7
  Reported by @danielbachhuber on 2016-01-08.
8
 
9
+ - BREAKING CHANGE: Removes compatibility repo for WordPress 4.3.
10
+
11
+ WordPress 4.4 is now the minimum supported WordPress version.
12
+
13
+ (props @danielbachhuber, [#1848](https://github.com/WP-API/WP-API/pull/1848))
14
+
15
+ - BREAKING CHANGE: Changes link relation for types and taxonomies.
16
+
17
+ In Beta 9, this link relation was introduced as `item`, which isn't correct. The relation has been changed to `https://api.w.org/items`.
18
+
19
+ (props @danielbachhuber, [#1853](https://github.com/WP-API/WP-API/pull/1853))
20
+
21
+ - BREAKING CHANGE: Introduces `edit` context for `wp/v2/types` and `wp/v2/taxonomies`.
22
+
23
+ Some fields have moved into this context, which require `edit_posts` and `manage_terms`, respectively.
24
+
25
+ (props @danielbachhuber, [#1894](https://github.com/WP-API/WP-API/pull/1894), [#1864](https://github.com/WP-API/WP-API/pull/1864))
26
+
27
+ - BREAKING CHANGE: Removes `post_format` as a term `_link` for Posts.
28
+
29
+ Post formats aren't a custom taxonomy in the eyes of the REST API.
30
+
31
+ (props @danielbachhuber, [#1854](https://github.com/WP-API/WP-API/pull/1854))
32
+
33
+ - Declares `parent` query param for Pages.
34
+
35
+ (props @danielbachhuber, [#1975](https://github.com/WP-API/WP-API/pull/1975))
36
+
37
+ - Permits logged-in users to query for media.
38
+
39
+ (props @danielbachhuber, [#1973](https://github.com/WP-API/WP-API/pull/1973))
40
+
41
+ - Removes duplicated query params from Terms controller.
42
+
43
+ (props @danielbachhuber, [#1963](https://github.com/WP-API/WP-API/pull/1963))
44
+
45
+ - Adds `include` param to `/wp/v2/posts`, `/wp/v2/users`, `/wp/v2/<taxonomy>` and `/wp/v2/comments`.
46
+
47
+ (props @danielbachhuber, [#1961](https://github.com/WP-API/WP-API/pull/1961), [#1964](https://github.com/WP-API/WP-API/pull/1964), [#1968](https://github.com/WP-API/WP-API/pull/1968), [#1971](https://github.com/WP-API/WP-API/pull/1971))
48
+
49
+ - Ensures `GET /wp/v2/posts` respects `order` and `orderby` params.
50
+
51
+ (props @danielbachhuber, [#1962](https://github.com/WP-API/WP-API/pull/1962))
52
+
53
+ - Fixes fatal by loading `wp-admin/includes/user.php` to expose `wp_delete_user()`.
54
+
55
+ (props @danielbachhuber, [#1958](https://github.com/WP-API/WP-API/pull/1958))
56
+
57
+ - Permits making a post sticky when also supplying an empty password.
58
+
59
+ (props @westonruter, [#1949](https://github.com/WP-API/WP-API/pull/1949))
60
+
61
+ - Uses `WP_REST_Request` internally across controllers.
62
+
63
+ (props @danielbachhuber, [#1933](https://github.com/WP-API/WP-API/pull/1933), [#1939](https://github.com/WP-API/WP-API/pull/1939), [#1934](https://github.com/WP-API/WP-API/pull/1934), [#1938](https://github.com/WP-API/WP-API/pull/1938))
64
+
65
+ - Cleans up permissions checks in `WP_REST_Terms_Controller`.
66
+
67
+ (props @danielbachhuber, [#1941](https://github.com/WP-API/WP-API/pull/1941))
68
+
69
+ - Uses `show_in_rest` to determine publicness for post types.
70
+
71
+ (props @danielbachhuber, [#1942](https://github.com/WP-API/WP-API/pull/1942))
72
+
73
+ - Makes `description` strings available for translation.
74
+
75
+ (props @danielbachhuber, [#1944](https://github.com/WP-API/WP-API/pull/1944))
76
+
77
+ - Checks `assign_terms` cap for taxonomy when managing post terms.
78
+
79
+ (props @danielbachhuber, [#1940](https://github.com/WP-API/WP-API/pull/1940))
80
+
81
+ - Defer to `edit_posts` of the custom post type when accessing private query vars.
82
+
83
+ (props @danielbachhuber, [#1886](https://github.com/WP-API/WP-API/pull/1886))
84
+
85
+ - Allows Terms collection params to be filtered.
86
+
87
+ (props @rachelbaker, [#1882](https://github.com/WP-API/WP-API/pull/1882))
88
+
89
+ - Renames post terms create/delete permissions callback.
90
+
91
+ (props @wpsmith, [#1923](https://github.com/WP-API/WP-API/pull/1923))
92
+
93
+ - Fixes invalid use of 'uri' as schema `type`.
94
+
95
+ (props @wpsmith, [#1913](https://github.com/WP-API/WP-API/pull/1913))
96
+
97
+ - Casts integer with (int) over intval for speed.
98
+
99
+ (props @wpsmith, [#1907](https://github.com/WP-API/WP-API/pull/1907))
100
+
101
+ - Fixes PHP Doc typo for `validate_schema_property` and `sanitize_schema_property`.
102
+
103
+ (props @wpsmith, @danielbachhuber, [#1909](https://github.com/WP-API/WP-API/pull/1909), [#1910](https://github.com/WP-API/WP-API/pull/1910))
104
+
105
+ - Adds a helpful description to the `filter` argument.
106
+
107
+ (props @danielbachhuber, [#1885](https://github.com/WP-API/WP-API/pull/1885))
108
+
109
+ - Changes order of Users response to match schema order.
110
+
111
+ (props @rachelbaker, [#1879](https://github.com/WP-API/WP-API/pull/1879))
112
+
113
+ - Adjusts Posts pagination headers for `filter` params.
114
+
115
+ (props @rachelbaker, [#1878](https://github.com/WP-API/WP-API/pull/1878))
116
+
117
+ - Uses proper status code when failing to get comments of private post.
118
+
119
+ (props @danielbachhuber, [#1866](https://github.com/WP-API/WP-API/pull/1867))
120
+
121
+ - Fixes invalid capability for comments get items permissions callback.
122
+
123
+ `manage_comments` doesn't exist; `moderate_comments` does.
124
+
125
+ (props @danielbachhuber, [#1866](https://github.com/WP-API/WP-API/pull/1866))
126
+
127
+ - Permits creating comments without an assigned post.
128
+
129
+ (props @danielbachhuber, [#1857](https://github.com/WP-API/WP-API/pull/1857))
130
+
131
+ - Prevents error notice when `show_in_rest` isn't set for a post type.
132
+
133
+ (props @danielbachhuber, [#1852](https://github.com/WP-API/WP-API/pull/1852))
134
+
135
+ ## 2.0 Beta 9.0
136
+
137
+ - BREAKING CHANGE: Move tags and categories to top-level endpoints.
138
+
139
+ Tags are now accessible at `/wp/v2/tags`, and categories accessible at `/wp/v2/categories`. Post terms reside at `/wp/v2/posts/<id>/tags` and `/wp/v2/<id>/categories`.
140
+
141
+ (props @danielbachhuber, [#1802](https://github.com/WP-API/WP-API/pull/1802))
142
+
143
+ - BREAKING CHANGE: Return object for requests to `/wp/v2/taxonomies`.
144
+
145
+ This is consistent with `/wp/v2/types` and `/wp/v2/statuses`.
146
+
147
+ (props @danielbachhuber, [#1825](https://github.com/WP-API/WP-API/pull/1825))
148
+
149
+ - BREAKING CHANGE: Remove `rest_get_timezone()`.
150
+
151
+ `json_get_timezone()` was only ever used in v1. This function causes fatals, and shouldn't be used.
152
+
153
+ (props @danielbachhuber, [#1823](https://github.com/WP-API/WP-API/pull/1823))
154
+
155
+ - BREAKING CHANGE: Rename `register_api_field()` to `register_rest_field()`.
156
+
157
+ Introduces a `register_api_field()` function for backwards compat, which calls `_doing_it_wrong()`. However, `register_api_field()` won't ever be committed to WordPress core, so you should update your function calls.
158
+
159
+ (props @danielbachhuber, [#1824](https://github.com/WP-API/WP-API/pull/1824))
160
+
161
+ - BREAKING CHANGE: Change taxonomies' `post_type` argument to `type`.
162
+
163
+ It's consistent with how we're exposing post types in the API.
164
+
165
+ (props @danielbachhuber, [#1824](https://github.com/WP-API/WP-API/pull/1824))
166
+
167
+ - Sync infrastructure with shipped in WordPress 4.4.
168
+
169
+ * `wp-includes/rest-api/rest-functions.php` is removed, and its functions moved into `wp-includes/rest-api.php`.
170
+ * Send nocache headers for REST requests. [#34832](https://core.trac.wordpress.org/ticket/34832)
171
+ * Fix handling of HEAD requests. [#34837](https://core.trac.wordpress.org/ticket/34837)
172
+ * Mark `WP_REST_Server::get_raw_data()` as static. [#34768](https://core.trac.wordpress.org/ticket/34768)
173
+ * Unabbreviate error string. [#34818](https://core.trac.wordpress.org/ticket/34818)
174
+
175
+ - Change terms endpoints to use `term_id` not `tt_id`.
176
+
177
+ (props @joehoyle, [#1837](https://github.com/WP-API/WP-API/pull/1837))
178
+
179
+ - Standardize declaration of `context` param for `GET` requests across controllers.
180
+
181
+ However, we're still inconsistent in which controllers expose which params. Follow [#1845](https://github.com/WP-API/WP-API/issues/1845) for further discussion.
182
+
183
+ (props @danielbachhuber, [#1795](https://github.com/WP-API/WP-API/pull/1795), [#1835](https://github.com/WP-API/WP-API/pull/1835), [#1838](https://github.com/WP-API/WP-API/pull/1838))
184
+
185
+ - Link types / taxonomies to their collections, and vice versa.
186
+
187
+ Collections link to their type / taxonomy with the `about` relation; types / taxonomies link to their colletion with the `item` relation, which is imperfect and may change in the future.
188
+
189
+ (props @danielbachhuber, [#1814](https://github.com/WP-API/WP-API/pull/1814), [#1817](https://github.com/WP-API/WP-API/pull/1817), [#1829](https://github.com/WP-API/WP-API/pull/1829). [#1846](https://github.com/WP-API/WP-API/pull/1846))
190
+
191
+ - Add missing 'wp/v2' in Location Response header when creating new Post Meta.
192
+
193
+ (props @johanmynhardt, [#1790](https://github.com/WP-API/WP-API/pull/1790))
194
+
195
+ - Expose Post collection query params, including `author`, `order`, `orderby` and `status`.
196
+
197
+ (props @danielbachhuber, [#1793](https://github.com/WP-API/WP-API/pull/1793))
198
+
199
+ - Ignore sticky posts by default.
200
+
201
+ (props @danielbachhuber, [#1801](https://github.com/WP-API/WP-API/pull/1801))
202
+
203
+ - Include `full` image size in attachment `sizes` attribute.
204
+
205
+ (props @danielbachhuber, [#1806](https://github.com/WP-API/WP-API/pull/1806))
206
+
207
+ - In text strings, use `id` instead of `ID`.
208
+
209
+ `ID` is an implementation artifact. Our Resources use `id`.
210
+
211
+ (props @danielbachhuber, [#1803](https://github.com/WP-API/WP-API/pull/1803))
212
+
213
+ - Ensure `attachment.sizes[]` use `mime_type` instead of `mime-type`.
214
+
215
+ (props @danielbachhuber, [#1809](https://github.com/WP-API/WP-API/pull/1809))
216
+
217
+ - Introduce `rest_authorization_required_code()`.
218
+
219
+ Many controllers returned incorrect HTTP codes, which this also fixes.
220
+
221
+ (props @danielbachhuber, [#1808](https://github.com/WP-API/WP-API/pull/1808))
222
+
223
+ - Respect core's `comment_registration` setting.
224
+
225
+ If it's enabled, require users to be logged in to comment.
226
+
227
+ (props @danielbachhuber, [#1826](https://github.com/WP-API/WP-API/pull/1826))
228
+
229
+ - Default to wildcard when searching users.
230
+
231
+ (props @danielbachhuber, [#1827](https://github.com/WP-API/WP-API/pull/1827))
232
+
233
+ - Bring the wp-api.js library up to date for v2 of the REST API.
234
+
235
+ (props @adamsilverstein, [#1828](https://github.com/WP-API/WP-API/pull/1828))
236
+
237
+ - Add `rest_prepare_status` filter.
238
+
239
+ (props @danielbachhuber, [#1830](https://github.com/WP-API/WP-API/pull/1830))
240
+
241
+ - Make `prepare_*` filters more consistent.
242
+
243
+ (props @danielbachhuber, [#1831](https://github.com/WP-API/WP-API/pull/1831))
244
+
245
+ - Add `rest_prepare_post_type` filter for post types.
246
+
247
+ (props @danielbachhuber, [#1833](https://github.com/WP-API/WP-API/pull/1833))
248
+
249
+ ## 2.0 Beta 8.0
250
+
251
+ - Prevent fatals when uploading attachment by including admin utilities.
252
+
253
+ (props @danielbachhuber, [#1756](https://github.com/WP-API/WP-API/pull/1756))
254
+
255
+ - Return 201 status code when creating a term.
256
+
257
+ (props @danielbachhuber, [#1753](https://github.com/WP-API/WP-API/pull/1753))
258
+
259
+ - Don't permit requesting terms cross routes.
260
+
261
+ Clients should only be able to request categories from the category route, and tags from the tag route.
262
+
263
+ (props @danielbachhuber, [#1764](https://github.com/WP-API/WP-API/pull/1764))
264
+
265
+ - Set `fields=>id` when using `WP_User_Query` to fix large memory usage
266
+
267
+ (props @joehoyle, [#1770](https://github.com/WP-API/WP-API/pull/1770))
268
+
269
+ - Fix Post `_link` to attached attachments.
270
+
271
+ (props @danielbachhuber, [#1777](https://github.com/WP-API/WP-API/pull/1777))
272
+
273
+ - Add support for getting a post with a custom public status.
274
+
275
+ (props @danielbachhuber, [#1765](https://github.com/WP-API/WP-API/pull/1765))
276
+
277
+ - Ensure post content doesn't get double-slashed on update.
278
+
279
+ (props @joehoyle, [#1772](https://github.com/WP-API/WP-API/pull/1772))
280
+
281
+ - Change 'int' to 'integer' for `WP_REST_Controller::validate_schema_property()`
282
+
283
+ (props @wpsmith, [#1759](https://github.com/WP-API/WP-API/pull/1759))
284
+
285
+ ## 2.0 Beta 7.0
286
+
287
+ - Sync infrastructure from WordPress core as of r35691.
288
+
289
+ * Remove `register_api_field()` because it's conceptually tied to `WP_REST_Controller` [#34730](https://core.trac.wordpress.org/ticket/34730)
290
+ * Update the REST API header links to use api.w.org [#34303](https://core.trac.wordpress.org/ticket/34303)
291
+ * Require the `$namespace` argument in `register_rest_route()` [#34416](https://core.trac.wordpress.org/ticket/34416)
292
+ * Include `enum` and `description` in help data [#34543](https://core.trac.wordpress.org/ticket/34543)
293
+ * Save `preg_match` iterations in `WP_REST_Server` [#34488](https://core.trac.wordpress.org/ticket/34488)
294
+ * Don't return route URL in `WP_REST_Request:get_params()` [#34647](https://core.trac.wordpress.org/ticket/34647)
295
+
296
+ - Restore `register_api_field()` within the plugin.
297
+
298
+ (props @danielbachhuber, [#1748](https://github.com/WP-API/WP-API/pull/1748))
299
+
300
+ - Require admin functions for use of `wp_handle_upload()`, fixing fatal.
301
+
302
+ (props @joehoyle, [#1746](https://github.com/WP-API/WP-API/pull/1746))
303
+
304
+ - Properly handle requesting terms where `parent=0` and `0` is a string.
305
+
306
+ (props @danielbachhuber, [#1739](https://github.com/WP-API/WP-API/pull/1739))
307
+
308
+ - Prevent PHP error notice when `&filter` isn't an array.
309
+
310
+ (props @danielbachhuber, [#1734](https://github.com/WP-API/WP-API/pull/1734))
311
+
312
+ - Change link relations to use api.w.org.
313
+
314
+ (props @danielbachhuber, [#1726](https://github.com/WP-API/WP-API/pull/1726))
315
+
316
+
317
+ ## 2.0 Beta 6.0
318
+
319
+ - Remove global inclusion of wp-admin/includes/admin.php
320
+
321
+ For a long time, the REST API loaded wp-admin/includes/admin.php to make use of specific admin utilities. Now, it only loads those admin utilities when it needs them.
322
+
323
+ If your custom endpoints make use of admin utilities, you'll need to make sure to load wp-admin/includes/admin.php before you use them.
324
+
325
+ (props @joehoyle, [#1696](https://github.com/WP-API/WP-API/pull/1696))
326
+
327
+ - Link directly to the featured image in a Post's links.
328
+
329
+ (props @rmccue, [#1563](https://github.com/WP-API/WP-API/pull/1563), [#1711](https://github.com/WP-API/WP-API/pull/1711))
330
+
331
+ - Provide object type as callback argument for custom API fields.
332
+
333
+ (props @jtsternberg, [#1714](https://github.com/WP-API/WP-API/pull/1714))
334
+
335
+ - Change users schema order to be order of importance instead of alpha.
336
+
337
+ (props @rachelbaker, [#1708](https://github.com/WP-API/WP-API/pull/1708))
338
+
339
+ - Clarify documentation for `date` and `modified` attributes.
340
+
341
+ (props @danielbachhuber, [#1715](https://github.com/WP-API/WP-API/pull/1715))
342
+
343
+ - Update the wp-api.js client from the client-js repo.
344
+
345
+ (props @rachelbaker, [#1709](https://github.com/WP-API/WP-API/pull/1709))
346
+
347
+ - Fix the `format` enum to be an array of strings.
348
+
349
+ (props @joehoyle, [#1707](https://github.com/WP-API/WP-API/pull/1707))
350
+
351
+ - Run revisions for collection through `prepare_response_for_collection()`.
352
+
353
+ (props @danielbachhuber, @rachelbaker, [#1671](https://github.com/WP-API/WP-API/pull/1671))
354
+
355
+ - Expose `date_gmt` for `view` context of Posts and Comments.
356
+
357
+ (props @danielbachhuber, [#1690](https://github.com/WP-API/WP-API/pull/1690))
358
+
359
+ - Fix PHP and JS docblock formatting.
360
+
361
+ (props @ahmadawais, [#1699](https://github.com/WP-API/WP-API/pull/1698), [#1699](https://github.com/WP-API/WP-API/pull/1699), [#1701](https://github.com/WP-API/WP-API/pull/1701), [#1700](https://github.com/WP-API/WP-API/pull/1700), [#1702](https://github.com/WP-API/WP-API/pull/1702), [#1703](https://github.com/WP-API/WP-API/pull/1703))
362
+
363
+ - Include `media_details` attribute for attachments in embed context.
364
+
365
+ For image attachments, media_details includes a sizes array of image sizes, which is useful for templating.
366
+
367
+ (props @danielbachhuber, [#1667](https://github.com/WP-API/WP-API/pull/1667))
368
+
369
+ - Make `WP_REST_Controller` error messages more helpful by specifying method to subclass.
370
+
371
+ (props @danielbachhuber, [#1670](https://github.com/WP-API/WP-API/pull/1670))
372
+
373
+ - Expose `slug` in `embed` context for Users.
374
+
375
+ `user_nicename` is a public attribute, used in user URLs, so this is safe data to present.
376
+
377
+ (props @danielbachhuber, [#1666](https://github.com/WP-API/WP-API/pull/1666))
378
+
379
+ - Handle falsy value from `wp_count_terms()`, fixing fatal.
380
+
381
+ (props @joehoyle, [#1641](https://github.com/WP-API/WP-API/pull/1641))
382
+
383
+ - Correct methods in `WP_REST_SERVER::EDITABLE` description.
384
+
385
+ (props @rachelbaker, [#1601](https://github.com/WP-API/WP-API/pull/1601))
386
+
387
+ - Add the embed context to Users collection query params.
388
+
389
+ (props @rachelbaker, [#1591](https://github.com/WP-API/WP-API/pull/1591))
390
+
391
+ - Add Terms Controller collection args details.
392
+
393
+ (props @rachelbaker, [#1603](https://github.com/WP-API/WP-API/pull/1603))
394
+
395
+ - Set comment author details from current user.
396
+
397
+ (props @rmccue, [#1580](https://github.com/WP-API/WP-API/pull/1580))
398
+
399
+ - More hook documentation.
400
+
401
+ (props @adamsilverstein, [#1556](https://github.com/WP-API/WP-API/pull/1556), [#1560](https://github.com/WP-API/WP-API/pull/1560))
402
+
403
+ - Return the trashed status of deleted posts/comments.
404
+
405
+ When a post or a comment is deleted, returns a flag to say whether it's been trashed or properly deleted.
406
+
407
+ (props @pento, [#1499](https://github.com/WP-API/WP-API/pull/1499))
408
+
409
+ - In `WP_REST_Posts_Controller::update_item()`, check the post ID based on the proper post type.
410
+
411
+ (props @rachelbaker, [#1497](https://github.com/WP-API/WP-API/pull/1497))
412
+
413
+ ## 2.0 Beta 5.0
414
+
415
+ - Load api-core as a compatibility library
416
+
417
+ Now api-core has been merged into WordPress trunk (for 4.4) we should no longer load the infrastructure code
418
+ when it's already available. This also fixes a fatal error for users who were on trunk.
419
+
420
+ (props @rmccue)
421
+
422
+ - Switch to new mysql_to_rfc3339
423
+
424
+ (props @rmccue)
425
+
426
+ - Double-check term taxonomy
427
+
428
+ (props @rmccue)
429
+
430
+ - Load admin functions
431
+
432
+ This was removed from the latest beta of WordPress in the REST API infrastructure, a more long term fix is planned.
433
+
434
+ (props @joehoyle)
435
+
436
+ - Add Add compat shim for renamed `rest_mysql_to_rfc3339()`
437
+
438
+ (props @danielbachhuber)
439
+
440
+ - Compat shim for `wp_is_numeric_array()`
441
+
442
+ (props @danielbachhuber)
443
+
444
+ - Revert Switch to register_post_type_args filter
445
+
446
+ (props @joehoyle)
447
+
448
+ ## 2.0 Beta 4.0
449
+
450
+ - Show public user information through the user controller.
451
+
452
+ In WordPress as of [r32683](https://core.trac.wordpress.org/changeset/32683) (scheduled for 4.3), `WP_User_Query` now has support for getting users with published posts.
453
+
454
+ To match current behaviour in WordPress themes and feeds, we now expose this public user information. This includes the avatar, description, user ID, custom URL, display name, and URL, for users who have published at least one post on the site. This information is available to all clients; other fields and data for all users are still only available when authenticated.
455
+
456
+ (props @joehoyle, @rmccue, @Shelob9, [#1397][gh-1397], [#839][gh-839], [#1435][gh-1435])
457
+
458
+ - Send schema in OPTIONS requests and index.
459
+
460
+ Rather than using separate `/schema` endpoints, the schema for items is now available through an OPTIONS request to the route. This means that full documentation is now available for endpoints through an OPTIONS request; this includes available methods, what data you can pass to the endpoint, and the data you'll get back.
461
+
462
+ This data is now also available in the main index and namespace indexes. Simply request the index with `context=help` to get full schema data. Warning: this response will be huge. The schema for single endpoints is also available in the collection's OPTIONS response.
463
+
464
+ **⚠️ This breaks backwards compatibility** for clients relying on schemas being at their own routes. These clients should instead send `OPTIONS` requests.
465
+
466
+ Custom endpoints can register their own schema via the `schema` option on the route. This option should live side-by-side with the endpoints (similar to `relation` in WP's meta queries), so your registration call will look something like:
467
+
468
+ ```php
469
+ register_rest_route( 'test-ns', '/test', array(
470
+ array(
471
+ 'methods' => 'GET',
472
+ 'callback' => 'my_test_callback',
473
+ ),
474
+
475
+ 'schema' => 'my_schema_callback',
476
+ ) );
477
+ ```
478
+
479
+ (props @rmccue, [#1415][gh-1415], [#1222][gh-1222], [#1305][gh-1305])
480
+
481
+ - Update JavaScript API for version 2.
482
+
483
+ Our fantastic JavaScript API from version 1 is now available for version 2, refreshed with the latest and greatest changes.
484
+
485
+ As a refresher: if you want to use it, simply make your script depend on `wp-api` when you enqueue it. If you want to enqueue the script manually, add `wp_enqueue_script( 'wp-api' )` to a callback on `wp_enqueue_scripts`.
486
+
487
+ (props @tlovett1, @kadamwhite, @nathanrice, [#1374][gh-1374], [#1320][gh-1320])
488
+
489
+ - Embed links inside items in a collection.
490
+
491
+ Previously when fetching a collection of items, you only received the items themselves. To fetch the links as well via embedding, you needed to make a request to the single item with `_embed` set.
492
+
493
+ No longer! You can now request a collection with embeds enabled (try `/wp/v2/posts?_embed`). This will embed links inside each item, allowing you to build interface items much easier (for example, post archive pages can get featured image data at the same time).
494
+
495
+ This also applies to custom endpoints. Any endpoint that returns a list of objects will automatically have the embedding applied to objects inside the list.
496
+
497
+ (props @rmccue, [#1459][gh-1459], [#865][gh-865])
498
+
499
+ - Fix potential XSS vulnerability.
500
+
501
+ Requests from other origins could potentially run code on the API domain, allowing cross-origin access to authentication cookies or similar.
502
+
503
+ Reported by @xknown on 2015-07-23.
504
+
505
+ - Move `/posts` `WP_Query` vars back to `filter` param.
506
+
507
+ In version 1, we had internal `WP_Query` vars available via `filter` (e.g. `filter[s]=search+term`). For our first betas of version 2, we tried something different and exposed these directly on the endpoint. The experiment has now concluded; we didn't like this that much, so `filter` is back.
508
+
509
+ We plan on adding nicer looking arguments to collections in future releases, with a view towards being consistent across different collections. We also plan on opening up the underlying query vars via `filter` for users, comments, and terms as well.
510
+
511
+ **⚠️ This breaks backwards compatibility** for users using WP Query vars. Simply change your `x=y` parameter to `filter[x]=y`.
512
+
513
+ (props @WP-API, [#1420][gh-1420])
514
+
515
+ - Respect `rest_base` for taxonomies.
516
+
517
+ **⚠️ This breaks backwards compatibility** by changing the `/wp/v2/posts/{id}/terms/post_tag` endpoint to `/wp/v2/posts/{id}/tag`.
518
+
519
+ (props @joehoyle, [#1466][gh-1466])
520
+
521
+ - Add permission check for retrieving the posts collection in edit context.
522
+
523
+ By extension of the fact that getting any individual post yields a forbidden context error when the `context=edit` and the user is not authorized, the user should also not be permitted to list any post items when unauthorized.
524
+
525
+ (props @danielpunkass, [#1412][gh-1412])
526
+
527
+ - Ensure the REST API URL always has a trailing slash.
528
+
529
+ Previously, when pretty permalinks were enabled, the API URL during autodiscovery looked like `/wp-json`, whereas the non-pretty permalink URL looked like `?rest_route=/`. These are now consistent, and always end with a slash character to simplify client URL building.
530
+
531
+ (props @danielpunkass, @rmccue, [#1426][gh-1426], [#1442][gh-1442], [#1455][gh-1455], [#1467][gh-1467])
532
+
533
+ - Use `wp_json_encode` instead of `json_encode`
534
+
535
+ Since WordPress 4.1, `wp_json_encode` has been available to ensure encoded values are sane, and that non-UTF8 encodings are supported. We now use this function rather than doing the encode ourselves.
536
+
537
+ (props @rmccue, @pento, [#1417][gh-1417])
538
+
539
+ - Add `role` to schema for users.
540
+
541
+ The available roles you can assign to a user are now available in the schema as an `enum`.
542
+
543
+ (props @joehoyle, [#1400][gh-1400])
544
+
545
+ - Use the schema for validation inside the comments controller.
546
+
547
+ Previously, the schema was merely a decorative element for documentation inside the comments controller. To bring it inline with our other controllers, the schema is now used internally for validation.
548
+
549
+ (props @joehoyle, [#1422][gh-1422])
550
+
551
+ - Don't set the Location header in update responses.
552
+
553
+ Previously, the Location header was sent when updating resources due to some inadvertent copypasta. This header should only be sent when creating to direct clients to the new resource, and isn't required when you're already on the correct resource.
554
+
555
+ (props @rachelbaker, [#1441][gh-1441])
556
+
557
+ - Re-enable the `rest_insert_post` action hook for `WP_REST_Posts_Controller`
558
+
559
+ This was disabled during 2.0 development to avoid breaking lots of plugins on the `json_insert_post` action. Now that we've changed namespaces and are Mostly Stable (tm), we can re-enable the action.
560
+
561
+ (props @jaredcobb, [#1427][gh-1427], [#1424][gh-1424])
562
+
563
+ - Fix post taxonomy terms link URLs.
564
+
565
+ When moving the routes in a previous beta, we forgot to correct the links on post objects to the new correct route. Sorry!
566
+
567
+ (props @rachelbaker, @joehoyle, [#1447][gh-1447], [#1383][gh-1383])
568
+
569
+ - Use `wp_get_attachment_image_src()` on the image sizes in attachments.
570
+
571
+ Since the first versions of the API, we've been building attachment URLs via `str_replace`. Who knows why we were doing this, but it caused problems with custom attachment URLs (such as CDN-hosted images). This now correctly uses the internal functions and filters.
572
+
573
+ (props @joehoyle, [#1462][gh-1462])
574
+
575
+ - Make the embed context a default, not forced.
576
+
577
+ If you want embeds to bring in full data rather than with `context=edit`, you can now change the link to specify `context=view` explicitly.
578
+
579
+ (props @rmccue, [#1464][gh-1464])
580
+
581
+ - Ensure we always use the `term_taxonomy_id` and never expose `term_id` publicly.
582
+
583
+ Previously, `term_id` was inadvertently exposed in some error responses.
584
+
585
+ (props @jdolan, [#1430][gh-1430])
586
+
587
+ - Fix adding alt text to attachments on creation.
588
+
589
+ Previously, this could only be set when updating an attachment, not when creating one.
590
+
591
+ (props @joehoyle, [#1398][gh-1398])
592
+
593
+ - Throw an error when registering routes without a namespace.
594
+
595
+ Namespaces should **always** be provided when registering routes. We now throw a `doing_it_wrong` error when attempting to register one. (Previously, this caused a warning, or an invalid internal route.)
596
+
597
+ If you *really* need to register namespaceless routes (e.g. to replicate an existing API), call `WP_REST_Server::register_route` directly rather than using the convenience function.
598
+
599
+ (props @joehoyle, @rmccue, [#1355][gh-1355])
600
+
601
+ - Show links on embeds.
602
+
603
+ Previously, links were accidentally stripped from embedded response data.
604
+
605
+ (props @rmccue, [#1472][gh-1472])
606
+
607
+ - Clarify insufficient permisssion error when editing posts.
608
+
609
+ (props @danielpunkass, [#1411][gh-1411])
610
+
611
+ - Improve @return inline docs for rest_ensure_response()
612
+
613
+ (props @Shelob9, [#1328][gh-1328])
614
+
615
+ - Check taxonomies exist before trying to set properties.
616
+
617
+ (props @joehoyle, @rachelbaker, [#1354][gh-1354])
618
+
619
+ - Update controllers to ensure we use `sanitize_callback` wherever possible.
620
+
621
+ (props @joehoyle, [#1399][gh-1399])
622
+
623
+ - Add more phpDoc documentation, and correct existing documentation.
624
+
625
+ (props @Shelob9, @rmccue, [#1432][gh-1432], [#1433][gh-1433], [#1465][gh-1465])
626
+
627
+ - Update testing infrastructure.
628
+
629
+ Travis now runs our coding standards tests in parallel, and now uses the new, faster container-based testing infrastructure.
630
+
631
+ (props @ntwb, @frozzare, [#1449][gh-1449], [#1457][gh-1457])
632
+
633
+ [View all changes](https://github.com/WP-API/WP-API/compare/2.0-beta3...2.0-beta4)
634
+
635
+ [gh-839]: https://github.com/WP-API/WP-API/issues/839
636
+ [gh-865]: https://github.com/WP-API/WP-API/issues/865
637
+ [gh-1222]: https://github.com/WP-API/WP-API/issues/1222
638
+ [gh-1305]: https://github.com/WP-API/WP-API/issues/1305
639
+ [gh-1310]: https://github.com/WP-API/WP-API/issues/1310
640
+ [gh-1320]: https://github.com/WP-API/WP-API/issues/1320
641
+ [gh-1328]: https://github.com/WP-API/WP-API/issues/1328
642
+ [gh-1354]: https://github.com/WP-API/WP-API/issues/1354
643
+ [gh-1355]: https://github.com/WP-API/WP-API/issues/1355
644
+ [gh-1372]: https://github.com/WP-API/WP-API/issues/1372
645
+ [gh-1374]: https://github.com/WP-API/WP-API/issues/1374
646
+ [gh-1383]: https://github.com/WP-API/WP-API/issues/1383
647
+ [gh-1397]: https://github.com/WP-API/WP-API/issues/1397
648
+ [gh-1398]: https://github.com/WP-API/WP-API/issues/1398
649
+ [gh-1399]: https://github.com/WP-API/WP-API/issues/1399
650
+ [gh-1400]: https://github.com/WP-API/WP-API/issues/1400
651
+ [gh-1402]: https://github.com/WP-API/WP-API/issues/1402
652
+ [gh-1411]: https://github.com/WP-API/WP-API/issues/1411
653
+ [gh-1412]: https://github.com/WP-API/WP-API/issues/1412
654
+ [gh-1413]: https://github.com/WP-API/WP-API/issues/1413
655
+ [gh-1415]: https://github.com/WP-API/WP-API/issues/1415
656
+ [gh-1417]: https://github.com/WP-API/WP-API/issues/1417
657
+ [gh-1420]: https://github.com/WP-API/WP-API/issues/1420
658
+ [gh-1422]: https://github.com/WP-API/WP-API/issues/1422
659
+ [gh-1424]: https://github.com/WP-API/WP-API/issues/1424
660
+ [gh-1426]: https://github.com/WP-API/WP-API/issues/1426
661
+ [gh-1427]: https://github.com/WP-API/WP-API/issues/1427
662
+ [gh-1430]: https://github.com/WP-API/WP-API/issues/1430
663
+ [gh-1432]: https://github.com/WP-API/WP-API/issues/1432
664
+ [gh-1433]: https://github.com/WP-API/WP-API/issues/1433
665
+ [gh-1435]: https://github.com/WP-API/WP-API/issues/1435
666
+ [gh-1441]: https://github.com/WP-API/WP-API/issues/1441
667
+ [gh-1442]: https://github.com/WP-API/WP-API/issues/1442
668
+ [gh-1447]: https://github.com/WP-API/WP-API/issues/1447
669
+ [gh-1449]: https://github.com/WP-API/WP-API/issues/1449
670
+ [gh-1455]: https://github.com/WP-API/WP-API/issues/1455
671
+ [gh-1455]: https://github.com/WP-API/WP-API/issues/1455
672
+ [gh-1457]: https://github.com/WP-API/WP-API/issues/1457
673
+ [gh-1459]: https://github.com/WP-API/WP-API/issues/1459
674
+ [gh-1462]: https://github.com/WP-API/WP-API/issues/1462
675
+ [gh-1464]: https://github.com/WP-API/WP-API/issues/1464
676
+ [gh-1465]: https://github.com/WP-API/WP-API/issues/1465
677
+ [gh-1466]: https://github.com/WP-API/WP-API/issues/1466
678
+ [gh-1467]: https://github.com/WP-API/WP-API/issues/1467
679
+ [gh-1472]: https://github.com/WP-API/WP-API/issues/1472
680
+
681
  ## 2.0 Beta 3.0
682
 
683
  - Add ability to declare sanitization and default options for schema fields.
README.md CHANGED
@@ -4,6 +4,7 @@ Access your WordPress site's data through an easy-to-use HTTP REST API.
4
 
5
  [![Build Status](https://travis-ci.org/WP-API/WP-API.svg?branch=develop)](https://travis-ci.org/WP-API/WP-API)
6
  [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/WP-API/WP-API/badges/quality-score.png?b=develop)](https://scrutinizer-ci.com/g/WP-API/WP-API/?branch=develop)
 
7
 
8
  ## WARNING
9
 
@@ -26,7 +27,7 @@ Retrieving or updating data is as simple as sending a HTTP request.
26
 
27
  Want to get your site's posts? Simply send a `GET` request to `/wp-json/wp/v2/posts`.
28
  Update user with ID 4? Send a `POST` request to `/wp-json/wp/v2/users/4`. Get all
29
- posts with the search term "awesome"? `GET /wp-json/wp/v2/posts?s=awesome`.
30
  It's that easy.
31
 
32
  WP API exposes a simple yet easy interface to WP Query, the posts API, post meta
@@ -49,6 +50,8 @@ There's no fixed timeline for integration into core at this time, but getting cl
49
  Drop this directory in and activate it. You need to be using pretty permalinks
50
  to use the plugin, as it uses custom rewrite rules to power the API.
51
 
 
 
52
  ## Issue Tracking
53
 
54
  All tickets for the project are being tracked on [GitHub][]. You can also take a
4
 
5
  [![Build Status](https://travis-ci.org/WP-API/WP-API.svg?branch=develop)](https://travis-ci.org/WP-API/WP-API)
6
  [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/WP-API/WP-API/badges/quality-score.png?b=develop)](https://scrutinizer-ci.com/g/WP-API/WP-API/?branch=develop)
7
+ [![codecov.io](http://codecov.io/github/WP-API/WP-API/coverage.svg?branch=develop)](http://codecov.io/github/WP-API/WP-API?branch=develop)
8
 
9
  ## WARNING
10
 
27
 
28
  Want to get your site's posts? Simply send a `GET` request to `/wp-json/wp/v2/posts`.
29
  Update user with ID 4? Send a `POST` request to `/wp-json/wp/v2/users/4`. Get all
30
+ posts with the search term "awesome"? `GET /wp-json/wp/v2/posts?filter[s]=awesome`.
31
  It's that easy.
32
 
33
  WP API exposes a simple yet easy interface to WP Query, the posts API, post meta
50
  Drop this directory in and activate it. You need to be using pretty permalinks
51
  to use the plugin, as it uses custom rewrite rules to power the API.
52
 
53
+ Also, be sure to use the Subversion `trunk` branch of WordPress Core as there are potentially recent commits to Core that the REST API relies on. See the [WordPress.org website](https://wordpress.org/download/svn/) for simple instructions.
54
+
55
  ## Issue Tracking
56
 
57
  All tickets for the project are being tracked on [GitHub][]. You can also take a
compatibility-v1.php DELETED
@@ -1,110 +0,0 @@
1
- <?php
2
-
3
- add_filter( 'json_endpoints', 'json_v1_compatible_routes', 1000 );
4
- add_filter( 'json_dispatch_request', 'json_v1_compatible_dispatch', 10, 3 );
5
-
6
- /**
7
- * Make version 1 routes compatible with v2
8
- *
9
- * @param array $routes API routes
10
- * @return array Filtered routes
11
- */
12
- function json_v1_compatible_routes( $routes ) {
13
- foreach ( $routes as $key => &$route ) {
14
- // Single, with new-style registration
15
- if ( isset( $route['callback'] ) || empty( $route ) ) {
16
- continue;
17
- }
18
-
19
- // Multiple, with new-style registration
20
- $first = reset( $route );
21
- if ( isset( $first['callback'] ) ) {
22
- continue;
23
- }
24
-
25
- // Old-style, map to new-style
26
- if ( count( $route ) <= 2 && isset( $route[1] ) && ! is_array( $route[1] ) ) {
27
- $route = array( $route );
28
- }
29
-
30
- foreach ( $route as &$handler ) {
31
- $methods = isset( $handler[1] ) ? $handler[1] : WP_REST_Server::METHOD_GET;
32
-
33
- $handler = array(
34
- 'callback' => $handler[0],
35
- 'methods' => $methods,
36
- 'v1_compat' => true,
37
- );
38
- }
39
- }
40
-
41
- return $routes;
42
- }
43
-
44
- /**
45
- * Use Reflection to match request parameters to function parameters
46
- *
47
- * @param mixed $result Result to use
48
- * @param WP_JSON_Request $request Request object
49
- * @return mixed
50
- */
51
- function json_v1_compatible_dispatch( $result, $request ) {
52
- // Allow other plugins to hijack too
53
- if ( null !== $result ) {
54
- return $result;
55
- }
56
-
57
- // Do we need the compatibility shim?
58
- $params = $request->get_attributes();
59
- if ( empty( $params['v1_compat'] ) ) {
60
- return $result;
61
- }
62
-
63
- // Build up the arguments, old-style
64
- $args = array_merge( $request->get_url_params(), $request->get_query_params() );
65
- if ( $request->get_method() === 'POST' ) {
66
- $args = array_merge( $args, $request->get_body_params() );
67
- }
68
-
69
- $args = json_v1_sort_callback_params( $params['callback'], $args );
70
- if ( is_wp_error( $args ) ) {
71
- return $args;
72
- }
73
-
74
- return call_user_func_array( $params['callback'], $args );
75
- }
76
-
77
- /**
78
- * Sort parameters by order specified in method declaration
79
- *
80
- * Takes a callback and a list of available params, then filters and sorts
81
- * by the parameters the method actually needs, using the Reflection API
82
- *
83
- * @param callback $callback
84
- * @param array $params
85
- * @return array
86
- */
87
- function json_v1_sort_callback_params( $callback, $provided ) {
88
- if ( is_array( $callback ) ) {
89
- $ref_func = new ReflectionMethod( $callback[0], $callback[1] );
90
- } else {
91
- $ref_func = new ReflectionFunction( $callback );
92
- }
93
-
94
- $wanted = $ref_func->getParameters();
95
- $ordered_parameters = array();
96
-
97
- foreach ( $wanted as $param ) {
98
- if ( isset( $provided[ $param->getName() ] ) ) {
99
- // We have this parameters in the list to choose from
100
- $ordered_parameters[] = $provided[ $param->getName() ];
101
- } elseif ( $param->isDefaultValueAvailable() ) {
102
- // We don't have this parameter, but it's optional
103
- $ordered_parameters[] = $param->getDefaultValue();
104
- } else {
105
- // We don't have this parameter and it wasn't optional, abort!
106
- return new WP_Error( 'json_missing_callback_param', sprintf( __( 'Missing parameter %s' ), $param->getName() ), array( 'status' => 400 ) );
107
- }
108
- }
109
- return $ordered_parameters;
110
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/README.md DELETED
@@ -1,26 +0,0 @@
1
- API Documentation
2
- =================
3
- Learn how the JSON REST API works from the ground up!
4
-
5
- First time interacting with the API? Start with the [Getting Started][] guide,
6
- which will introduce you to the basic concepts for working with the API.
7
-
8
- From there, progress on to other [guides][] to learn in detail about parts of
9
- the API.
10
-
11
- Take a look at more detailed information on [post][post-routes] or
12
- [media][media-routes], or read about [maximizing compatibility][compatibility]
13
- with older clients.
14
-
15
- Dive in deeper into the [schema details][schema] to better understand the little
16
- details, or read about the [philosophy][] behind them. Read about the
17
- [implementation details][implementation] on how the API works internally.
18
-
19
- [Getting Started]: http://wp-api.org/guides/getting-started.html
20
- [guides]: http://wp-api.org/guides.html
21
- [post-routes]: http://wp-api.org/#posts
22
- [media-routes]: http://wp-api.org/#media
23
- [compatibility]: compatibility.md
24
- [schema]: schema.md
25
- [philosophy]: internals/philosophy.md
26
- [implementation]: internals/implementation.md
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/routes/routes.md DELETED
@@ -1,1569 +0,0 @@
1
- Posts
2
- =====
3
-
4
- Create a Post
5
- -------------
6
-
7
- POST /posts
8
-
9
- Requires [authentication](http://wp-api.org/guides/authentication.html)
10
-
11
- ### Input
12
- The `data` parameter consists of the elements of the Post object to be
13
- created. This data can be submitted via a regular HTTP multipart body, with
14
- the Post keys and values set to the `data` parameter, or through a direct JSON
15
- body.
16
-
17
- That is, the following are equivalent:
18
-
19
- ```
20
- Content-Type: application/x-www-form-urlencoded
21
-
22
- data[title]=Hello%20World!&data[content_raw]=Content&data[excerpt_raw]=Excerpt
23
- ```
24
-
25
- ```
26
- Content-Type: application/json
27
-
28
- {"title":"Hello World!","content_raw":"Content","excerpt_raw":"Excerpt"}
29
- ```
30
-
31
- The `data` parameter should be an object containing the following key value
32
- pairs:
33
-
34
- * `title` - Title of the post. (string) __*required*__
35
- * `content_raw` - Full text of the post. (string) __*required*__
36
- * `excerpt_raw` - Text for excerpt of the post. (string) *optional*
37
- * `name` - Slug of the post. (string) *optional*
38
- * `status` - Post status of the post: `draft`, `publish`, `pending`, `future`,
39
- `private`, or any custom registered status. If providing a status of
40
- `future`, you must specify a `date` in order for the post to be published as
41
- expected. Default is `draft`. (string) *optional*
42
- * `type` - Post type of the post: `post`, `page`, `link`, `nav_menu_item`, or
43
- a any custom registered type. Default is `post`. (string) *optional*
44
- * `date` - Date and time the post was, or should be, published in local time.
45
- Date should be an RFC3339 timestamp](http://tools.ietf.org/html/rfc3339).
46
- Example: 2014-01-01T12:20:52Z. Default is the local date and time. (string)
47
- *optional*
48
- * `date_gmt` - Date and time the post was, or should be, published in UTC time.
49
- Date should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339).
50
- Example: 201401-01T12:20:52Z. Default is the current GMT date and time.
51
- (string) *optional*
52
- * `author` - Author of the post. Author can be provided as a string of the
53
- author's ID or as the User object of the author. Default is current user.
54
- (object \| string) *optional*
55
- * `password` - Password for protecting the post. Default is empty string.
56
- (string) *optional*
57
- * `post_parent` - Post ID of the post parent. Default is 0. (integer)
58
- *optional*
59
- * `post_format` - Format of the post. Default is `standard`. (string)
60
- *optional*
61
- * `menu_order` - The order in which posts specified as the `page` type should
62
- appear in supported menus. Default 0. (integer) *optional*
63
- * `comment_status` - Comment status for the post: `open` or `closed`.
64
- Indicates whether users can submit comments to the post. Default is the
65
- option 'default_comment_status', or 'closed'. (string) *optional*
66
- * `ping_status` - Ping status for the post: `open` or `closed`. Indicates
67
- whether users can submit pingbacks or trackbacks to the post. Default is the
68
- option 'default_ping_status'. (string) *optional*
69
- * `sticky` - Sticky status for the post: `true` or `false`. Default is
70
- `false`. (boolean) *optional*
71
- * `post_meta` - Post meta entries of the post. Post meta should be an array
72
- of one or more Meta objects for each post meta entry. See the Create Meta
73
- for a Post endpoint for the key value pairs. (array) *optional*
74
-
75
-
76
- ### Response
77
- On a successful creation, a 201 Created status is given, indicating that the
78
- post has been created. The post is available canonically from the URL specified
79
- in the Location header.
80
-
81
- The new Post entity is also returned in the body for convienience.
82
-
83
- If the client is not authenticated, a 403 Forbidden response is given.
84
-
85
- Retrieve Posts
86
- --------------
87
- The Posts endpoint returns a Post Collection containing a subset of the site's
88
- posts.
89
-
90
- GET /posts
91
-
92
- ### Input
93
- #### `filter`
94
- The `filter` parameter controls the parameters used to query for posts.
95
-
96
- **Note:** Only "public" query variables are available via the API, as not all
97
- query variables are safe to expose. "Private" query variables are also available
98
- when authenticated as a user with `edit_posts`. Other query variables can be
99
- registered via the `query_vars` filter, or `json_query_vars` for API-specific
100
- query variables.
101
-
102
- Extended documentation on the query variables is available from
103
- [the codex](http://codex.wordpress.org/Class_Reference/WP_Query).
104
-
105
- The following query variables are available to the API:
106
-
107
- * `m`
108
- * `p`
109
- * `posts`
110
- * `w`
111
- * `cat`
112
- * `withcomments`
113
- * `withoutcomments`
114
- * `s`
115
- * `search`
116
- * `exact`
117
- * `sentence`
118
- * `calendar`
119
- * `page`
120
- * `paged`
121
- * `more`
122
- * `tb`
123
- * `pb`
124
- * `author`
125
- * `order`
126
- * `orderby`
127
- * `year`
128
- * `monthnum`
129
- * `day`
130
- * `hour`
131
- * `minute`
132
- * `second`
133
- * `name`
134
- * `category_name`
135
- * `tag`
136
- * `feed`
137
- * `author_name`
138
- * `static`
139
- * `pagename`
140
- * `page_id`
141
- * `error`
142
- * `comments_popup`
143
- * `attachment`
144
- * `attachment_id`
145
- * `subpost`
146
- * `subpost_id`
147
- * `preview`
148
- * `robots`
149
- * `taxonomy`
150
- * `term`
151
- * `cpage`
152
- * `posts_per_page`
153
-
154
- In addition, the following are available when authenticated as a user with
155
- `edit_posts`:
156
-
157
- * `offset`
158
- * `posts_per_archive_page`
159
- * `showposts`
160
- * `nopaging`
161
- * `post_type`
162
- * `post_status`
163
- * `category__in`
164
- * `category__not_in`
165
- * `category__and`
166
- * `tag__in`
167
- * `tag__not_in`
168
- * `tag__and`
169
- * `tag_slug__in`
170
- * `tag_slug__and`
171
- * `tag_id`
172
- * `post_mime_type`
173
- * `perm`
174
- * `comments_per_page`
175
- * `post__in`
176
- * `post__not_in`
177
- * `post_parent`
178
- * `post_parent__in`
179
- * `post_parent__not_in`
180
-
181
- ```
182
- GET /posts?filter[posts_per_page]=8&filter[order]=ASC
183
- ```
184
-
185
- #### `context`
186
- The `context` parameter controls the format of the data to return. See the
187
- Retrieve a Post endpoint for available contexts.
188
-
189
- Default is "view". (string)
190
-
191
-
192
- #### `type`
193
- The `type` parameter specifies the post type to retrieve. This can either be a
194
- string or an array of types.
195
-
196
- Note that arrays are specified using the `[]` URL syntax. e.g.
197
-
198
- ```
199
- GET /posts?type[]=post&type[]=page
200
- ```
201
-
202
- Default is "post". (string)
203
-
204
-
205
- ### Response
206
- The response is a Post Collection document containing the requested Posts if
207
- available.
208
-
209
-
210
- Retrieve a Post
211
- ---------------
212
-
213
- GET /posts/<id>
214
-
215
- ### Input
216
- #### `context`
217
- The `context` parameter controls the format of the data to return. The
218
- following contexts are available:
219
-
220
- * `view`: The default context. Gives the normal User entity.
221
- * `edit`: Context used for extra fields relevant to updating a user. Includes
222
- the `title_raw`, `content_raw`, `guid_raw` and `post_meta` fields, suitable
223
- for editing the post.
224
- * `parent`: Context used when embedding the response inside another (e.g. post
225
- author). This is intended as a minimal subset of the user data to reduce
226
- response size. Returns the `parent` field as an ID, rather than an embedded
227
- post, to ensure we don't traverse the entire post hierarchy.
228
-
229
- ### Response
230
- The response is a Post entity containing the requested Post if available. The
231
- fields available on the Post depend on the `context` parameter.
232
-
233
-
234
- Edit a Post
235
- -----------
236
-
237
- PUT /posts/<id>
238
-
239
- Requires [authentication](http://wp-api.org/guides/authentication.html)
240
-
241
- For compatibility reasons, this endpoint also accepts the POST and PATCH
242
- methods. Both of these methods have the same behaviour as using PUT. It is
243
- recommended to use PUT if available to fit with REST convention.
244
-
245
- ### Input
246
- The `data` parameter consists of Post ID and the elements of the Post object
247
- to be modified. This data can be submitted via a regular HTTP multipart body,
248
- with the Post keys and values set to the `data` parameter, or through a direct
249
- JSON body. See the Create Post endpoint for an example.
250
-
251
- The `data` parameter should be an object containing the following key value
252
- pairs:
253
-
254
- * `ID` - Unique ID of the post. (integer) __*required*__
255
- * `title` - Title of the post. (string) __*required*__
256
- * `content_raw` - Full text of the post. (string) __*required*__
257
- * `excerpt_raw` - Text for excerpt of the post. (string) *optional*
258
- * `name` - Slug of the post. (string) *optional*
259
- * `status` - Post status of the post: `draft`, `publish`, `pending`, `future`,
260
- `private`, or any custom registered status. If providing a status of
261
- `future`, you must specify a `date` in order for the post to be published as
262
- expected. Default is `draft`. (string) *optional*
263
- * `type` - Post type of the post: `post`, `page`, `link`, `nav_menu_item`, or
264
- a any custom registered type. Default is `post`. (string) *optional*
265
- * `date` - Date and time the post was, or should be, published in local time.
266
- Date should be an RFC3339 timestamp](http://tools.ietf.org/html/rfc3339).
267
- Example: 2014-01-01T12:20:52Z. Default is the local date and time. (string)
268
- *optional*
269
- * `date_gmt` - Date and time the post was, or should be, published in UTC time.
270
- Date should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339).
271
- Example: 201401-01T12:20:52Z. Default is the current GMT date and time.
272
- (string) *optional*
273
- * `author` - Author of the post. Author can be provided as a string of the
274
- author's ID or as the User object of the author. Default is current user.
275
- (object \| string) *optional*
276
- * `password` - Password for protecting the post. Default is empty string.
277
- (string) *optional*
278
- * `post_parent` - Post ID of the post parent. Default is 0. (integer)
279
- *optional*
280
- * `post_format` - Format of the post. Default is `standard`. (string)
281
- *optional*
282
- * `menu_order` - The order in which posts specified as the `page` type should
283
- appear in supported menus. Default 0. (integer) *optional*
284
- * `comment_status` - Comment status for the post: `open` or `closed`.
285
- Indicates whether users can submit comments to the post. Default is the
286
- option 'default_comment_status', or 'closed'. (string) *optional*
287
- * `ping_status` - Ping status for the post: `open` or `closed`. Indicates
288
- whether users can submit pingbacks or trackbacks to the post. Default is the
289
- option 'default_ping_status'. (string) *optional*
290
- * `sticky` - Sticky status for the post: `true` or `false`. Default is
291
- `false`. (boolean) *optional*
292
- * `post_meta` - Post meta entries of the post. Post meta should be an array
293
- of one or more Meta objects for each post meta entry. See the Edit Meta
294
- for a Post endpoint for the key value pairs. (array) *optional*
295
-
296
-
297
- ### Response
298
- On a successful update, a 200 OK status is given, indicating the post has been
299
- updated. The updated Post entity is returned in the body.
300
-
301
- If the client is not authenticated, a 403 Forbidden response is sent.
302
-
303
- Delete a Post
304
- -------------
305
-
306
- DELETE /posts/<id>
307
-
308
- Requires [authentication](http://wp-api.org/guides/authentication.html)
309
-
310
- ### Input
311
- #### `force`
312
- The `force` parameter controls whether the post is permanently deleted or not.
313
- By default, this is set to false, indicating that the post will be sent to an
314
- intermediate storage (such as the trash) allowing it to be restored later. If
315
- set to true, the post will not be able to be restored by the user.
316
-
317
- Default is false. (boolean)
318
-
319
- ### Response
320
- On successful deletion, a 202 Accepted status code will be returned, indicating
321
- that the post has been moved to the trash for permanent deletion at a
322
- later date.
323
-
324
- If force was set to true, a 200 OK status code will be returned instead,
325
- indicating that the post has been permanently deleted.
326
-
327
- If the client is not authenticated, a 403 Forbidden status code will be returned.
328
-
329
- Retrieve Revisions for a Post
330
- ------------------------
331
-
332
- GET /posts/<id>/revisions
333
-
334
- Requires [authentication](http://wp-api.org/guides/authentication.html)
335
-
336
- ### Response
337
- If successful, returns a 200 OK status code and revisions for the given post.
338
-
339
- If the client is not authenticated, a 403 Forbidden status code will be returned.
340
-
341
-
342
- Create Meta for a Post
343
- ------------------------
344
-
345
- POST /posts/<id>/meta
346
-
347
- Requires [authentication](http://wp-api.org/guides/authentication.html)
348
-
349
- Note that the access rules for metadata apply here (see [Retrieve Meta for
350
- a Post](http://wp-api.org/#posts_retrieve-meta-for-a-post) ). Any submitted data that violates an access rule (e.g. sending
351
- serialized data) will result in a 403 error.
352
-
353
- ### Input
354
- The supplied data should be a Meta object. This data can be submitted via a
355
- regular HTTP multipart body, with the Meta key and value set with the `data`
356
- parameter, or through a direct JSON body.
357
-
358
- The `data` parameter should be an object containing the following key value
359
- pairs:
360
-
361
- * `key` - The post meta key to be created. (string) *required*
362
- * `value` - The post meta value for the key provided. (string) *required*
363
-
364
- ### Response
365
- On a successful creation, a 201 Created status is given, indicating that the
366
- Meta has been created. The post meta is available canonically from the URL
367
- specified in the Location header.
368
-
369
- The new Meta entity is also returned in the body for convienience.
370
-
371
- If the client is not authenticated, a 403 Forbidden status code will be returned.
372
-
373
- Retrieve Meta for a Post
374
- ------------------------
375
-
376
- GET /posts/<id>/meta
377
-
378
- Requires [authentication](http://wp-api.org/guides/authentication.html)
379
-
380
- WordPress metadata follows some special rules for access:
381
-
382
- * Metadata is only available to authenticated clients, as the fields are "raw"
383
- values from the database. The API cannot ensure that it's not leaking private
384
- data, although we're working on changing WordPress to support this.
385
-
386
- * "Complex" metadata is not available from the API. Only simple values, such as
387
- numbers, strings, and booleans, are available via the meta endpoints. Complex
388
- values, such as arrays and objects do not have a lossless (one-to-one)
389
- representation in JSON. Exposing the serialized value could leak internal
390
- implementation details and pose a security risk.
391
-
392
- * "Protected" metadata is not available from the API. This includes any metadata
393
- with a key prefixed with `_`, as well as any meta marked as protected by
394
- plugins. Protected meta is used to store internal data by many plugins and
395
- cannot be exposed to external clients.
396
-
397
- ### Response
398
- The response is a Meta entity containing all the post_meta for the specified
399
- Post if available.
400
-
401
- Returns a 403 Forbidden status code if the client is not authenticated.
402
-
403
- Retrieve a Meta for a Post
404
- ------------------------
405
-
406
- GET /posts/<id>/meta/<mid>
407
-
408
- Requires [authentication](http://wp-api.org/guides/authentication.html)
409
-
410
- Note that the access rules for metadata apply here (see [Retrieve Meta for
411
- a Post](http://wp-api.org/#posts_retrieve-meta-for-a-post) ).
412
-
413
- ### Response
414
- The response is a Meta entity containing the post_meta for the specified Meta and
415
- Post if available.
416
-
417
- Returns a 403 Forbidden status code if the client is not authenticated.
418
-
419
- Edit a Meta for a Post
420
- ------------------------
421
-
422
- PUT /posts/<id>/meta/<mid>
423
-
424
- Requires [authentication](http://wp-api.org/guides/authentication.html)
425
-
426
- Note that the access rules for metadata apply here (see [Retrieve Meta for
427
- a Post](http://wp-api.org/#posts_retrieve-meta-for-a-post) ). Any submitted data that violates an access rule (e.g. sending
428
- serialized data) will result in a 403 error.
429
-
430
- ### Input
431
- The supplied data should be a Meta object. This data can be submitted via a
432
- regular HTTP multipart body, with the Meta key and value set with the `data`
433
- parameter, or through a direct JSON body.
434
-
435
- The `data` parameter should be an array containing the following key value pairs:
436
-
437
- * `key` - The post meta key to be updated. (string) *required*
438
- * `value` - The post meta value for the key provided. (string) *required*
439
-
440
- ### Response
441
- On a successful update, a 200 OK status is given, indicating the post_meta has
442
- been updated. The updated Meta entity is returned in the body.
443
-
444
- If the client is not authenticated, a 403 Forbidden status code is returned.
445
-
446
- Delete a Meta for a Post
447
- -------------
448
-
449
- DELETE /posts/<id>/meta/<mid>
450
-
451
- Requires [authentication](http://wp-api.org/guides/authentication.html)
452
-
453
- Note that the access rules for metadata apply here (see Retrieve Meta for
454
- a Post). Attempting to delete data that violates an access rule (e.g. sending
455
- serialized data) will result in a 403 error.
456
-
457
- ### Response
458
- On successful deletion, a 200 OK status code will be returned, indicating
459
- that the post_meta has been permanently deleted.
460
-
461
- If the client is not authenticated, a 403 Forbidden status code is returned.
462
-
463
- Media
464
- =====
465
-
466
-
467
- Create an Attachment
468
- --------------------
469
- The Create Attachment endpoint is used to create the raw data for an attachment.
470
- This is a binary object (blob), such as image data or a video.
471
-
472
- POST /media
473
-
474
- Requires [authentication](http://wp-api.org/guides/authentication.html)
475
-
476
- ### Input
477
- The attachment creation endpoint can accept data in two forms.
478
-
479
- The primary input method accepts raw data POSTed with the corresponding content
480
- type set via the `Content-Type` HTTP header. This is the preferred submission
481
- method.
482
-
483
- The secondary input method accepts data POSTed via `multipart/form-data`, as per
484
- [RFC 2388][]. The uploaded file should be submitted with the name field set to
485
- "file", and the filename field set to the relevant filename for the file.
486
-
487
- In addition, a `Content-MD5` header can be set with the MD5 hash of the file, to
488
- enable the server to check for consistency errors. If the supplied hash does not
489
- match the hash calculated on the server, a 412 Precondition Failed header will
490
- be issued.
491
-
492
- [RFC 2388]: http://tools.ietf.org/html/rfc2388
493
-
494
- ### Response
495
- On a successful creation, a 201 Created status is given, indicating that the
496
- attachment has been created. The attachment is available canonically from the
497
- URL specified in the Location header.
498
-
499
- The new Attachment entity is also returned in the body for convienience.
500
-
501
- Returns a 403 Forbidden status code if the client is not authenticated.
502
-
503
- Get Attachments
504
- ---------------
505
- The Attachments endpoint returns an Attachment collection containing a subset of
506
- the site's attachments.
507
-
508
- This endpoint is an extended version of the Post retrieval endpoint.
509
-
510
- GET /media
511
-
512
- ### Input
513
- #### `fields`
514
- ...
515
-
516
- ### Response
517
- The response is an Attachment entity containing the requested Attachment if
518
- available.
519
-
520
-
521
- Users
522
- =====
523
-
524
-
525
- Create a User
526
- -------------
527
-
528
- POST /users
529
-
530
- Requires [authentication](http://wp-api.org/guides/authentication.html)
531
-
532
- ### Input
533
- The supplied data should be a User object. This data can be submitted via a
534
- regular HTTP multipart body, with User values set as values to the `data`
535
- parameter, or through a direct JSON body.
536
-
537
- That is, the following are equivalent:
538
-
539
- Content-Type: application/x-www-form-urlencoded
540
-
541
- data[username]=newuser&data[name]=New%20User&data[password]=secret
542
-
543
-
544
- Content-Type: application/json
545
-
546
- {"username":"newuser","name":"New User","password":"secret"}
547
-
548
- ### Response
549
- On a successful creation, a 201 Created status is given, indicating that the
550
- user has been created. The user is available canonically from the URL specified
551
- in the Location header.
552
-
553
- The new User entity is also returned in the body for convenience.
554
-
555
- A 403 Forbidden status is returned if the client is not authenticated.
556
-
557
- Retrieve Users
558
- --------------
559
- The Users endpoint returns a User Collection containing a subset of the site's
560
- users.
561
-
562
- GET /users
563
-
564
- Requires [authentication](http://wp-api.org/guides/authentication.html)
565
-
566
-
567
- ### Input
568
- #### `filter`
569
- The `filter` parameter controls the query parameters. It is essentially a subset
570
- of the parameters available to [`WP_User_Query`](http://codex.wordpress.org/Class_Reference/WP_User_Query).
571
-
572
- The parameter should be an array of the following key/value pairs:
573
-
574
- * `number` - Number of users to retrieve, use `-1` for all users. Default
575
- is set by the site. (integer)
576
- * `offset` - Number of users to skip. Default is 0. (integer)
577
- * `orderby` - Parameter to search by, as per [`WP_User_Query`](https://codex.wordpress.org/Class_Reference/WP_User_Query#Order_.26_Orderby_Parameters).
578
- Default is "user_login". (string)
579
- * `order` - Order to sort by. Default is "ASC". (string, "ASC" or "DESC")
580
- * `s` - Keyword to search for. (string)
581
-
582
- ### Response
583
- The response is a User Collection document containing the requested Users if
584
- available.
585
-
586
- A 403 Forbidden status is returned if the client is not authenticated.
587
-
588
-
589
- Retrieve a User
590
- ---------------
591
-
592
- GET /users/<id>
593
-
594
- Requires [authentication](http://wp-api.org/guides/authentication.html)
595
-
596
- ### Input
597
- #### `context`
598
- The `context` parameter controls the format of the data to return. The following
599
- contexts are available:
600
-
601
- * `view`: The default context. Gives the normal User entity.
602
- * `edit`: Context used for extra fields relevant to updating a user. Includes
603
- the `extra_capabilities` field; this field contains the capabilities assigned
604
- to the user themselves, rather than those inherited from their roles. Requires [authentication](http://wp-api.org/guides/authentication.html).
605
- * `embed`: Context used when embedding the response inside another (e.g. post
606
- author). This is intended as a minimal subset of the user data to reduce
607
- response size. Excludes `roles` and `capabilities`.
608
-
609
- Default is "view". (string)
610
-
611
- ### Response
612
- The response is a User entity containing the requested User if available. The
613
- fields available on the User depend on the `context` parameter.
614
-
615
- A 403 Forbidden status is returned if the client is not authenticated.
616
-
617
-
618
- Retrieve Current User
619
- -------------
620
-
621
- GET /users/me
622
-
623
- Requires [authentication](http://wp-api.org/guides/authentication.html)
624
-
625
- This endpoint offers a permalink to get the current user, without needing to
626
- know the user's ID.
627
-
628
- ### Input
629
- #### `context`
630
- The `context` parameter controls the format of the data to return. See the
631
- Retrieve a User endpoint for available contexts.
632
-
633
- Default is "view". (string)
634
-
635
- ### Response
636
- If the client is currently logged in, a 302 Found status is given. The User is
637
- available canonically from the URL specified in the Location header.
638
-
639
- The User entity containing the current User is also returned in the body for
640
- convenience. The fields available on the User depend on the `context` parameter.
641
-
642
- If the client is not logged in, a 403 Forbidden status is given.
643
-
644
-
645
- Edit a User
646
- -----------
647
-
648
- PUT /users/<id>
649
-
650
- Requires [authentication](http://wp-api.org/guides/authentication.html)
651
-
652
- For compatibility reasons, this endpoint also accepts the POST and PATCH
653
- methods. Both of these methods have the same behaviour as using PUT. It is
654
- recommended to use PUT if available to fit with REST convention.
655
-
656
- ### Input
657
- The supplied data should be a User object. This data can be submitted via a
658
- regular HTTP multipart body, with User values set as values to the `data`
659
- parameter, or through a direct JSON body. See the Create User endpoint for an
660
- example.
661
-
662
- ### Response
663
- On a successful update, a 200 OK status is given, indicating the user has been
664
- updated. The updated User entity is returned in the body.
665
-
666
- If the client is not logged in, a 403 Forbidden status is given.
667
-
668
- Delete a User
669
- -------------
670
-
671
- DELETE /users/<id>
672
-
673
- Requires [authentication](http://wp-api.org/guides/authentication.html)
674
-
675
- ### Input
676
- #### `force`
677
- The `force` parameter controls whether the user is permanently deleted or not.
678
- By default, this is set to false, indicating that the user will be sent to an
679
- intermediate storage (such as the trash) allowing it to be restored later. If
680
- set to true, the user will not be able to be restored.
681
-
682
- Default is false. (boolean)
683
-
684
- #### `reassign`
685
- The `reassign` parameter controls whether the deleted user's content is
686
- reassigned to a new User or not. If set to `null`, the deleted user's content
687
- will not be reassigned.
688
-
689
- Default is null. (integer)
690
-
691
-
692
- ### Response
693
- On successful deletion, a 202 Accepted status code will be returned, indicating
694
- that the user has been moved to the trash for permanent deletion at a
695
- later date.
696
-
697
- If force was set to true, a 200 OK status code will be returned instead,
698
- indicating that the user has been permanently deleted.
699
-
700
- If the client is not authenticated, a 403 Forbidden status is given.
701
-
702
- Taxonomies
703
- ==========
704
-
705
-
706
- Retrieve All Taxonomies
707
- -----------------------
708
- The Taxonomies endpoint returns a collection containing objects for each of the
709
- site's registered taxonomies.
710
-
711
- GET /taxonomies
712
-
713
-
714
- ### Response
715
- The response is a collection document containing all registered taxonomies.
716
-
717
-
718
- Retrieve a Taxonomy
719
- -------------------
720
-
721
- GET /taxonomies/<taxonomy>
722
-
723
- ### Response
724
- The response is a Taxonomy entity containing the requested Taxonomy, if available.
725
-
726
-
727
- Retrieve Terms for a Taxonomy
728
- -----------------------------
729
-
730
- GET /taxonomies/<taxonomy>/terms
731
-
732
- ### Response
733
- The response is a collection of taxonomy terms for the specified Taxonomy, if
734
- available.
735
-
736
- Retrieve a Taxonomy Term
737
- ------------------------
738
-
739
- GET /taxonomies/<taxonomy>/terms/<id>
740
-
741
- ### Response
742
- The response is a Taxonomy entity object containing the Taxonomy with the
743
- requested ID, if available.
744
-
745
- SCHEMA
746
- ============
747
- The API is designed around two types of responses: entities, and collections.
748
- Entities are JSON objects representing internal objects, both abstract and
749
- WordPress objects. Collections are JSON arrays of Entities.
750
-
751
- This document is for clients and providers wanting to ensure full compliance
752
- with the specification.
753
-
754
-
755
- Definitions
756
- ==========
757
- The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
758
- "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be
759
- interpreted as described in [RFC2119][].
760
-
761
- * Provider: A site making the API available for use
762
- * Consumer: An application accessing and interacting with the API
763
- * slug: A URL-friendly human-readable identifier, usually derived from the title
764
- of the entity.
765
-
766
- [RFC2119]: http://tools.ietf.org/html/rfc2119
767
-
768
-
769
- ### ABNF
770
- Augmented Backus-Naur Form (ABNF) is to be interpreted as described in
771
- [RFC5234][]. In addition, the following basic rules are used to describe basic
772
- parsing constructs above the standard JSON parsing rules.
773
-
774
- token = 1*<any OCTET except CTLs> ; DQUOTE must be escaped with "\"
775
-
776
- Note that as per ABNF, literal strings are case insensitive. That is:
777
-
778
- example-field = "id"
779
- example-field = "ID"
780
-
781
- Providers SHOULD use the capitalisation as per this specification to ensure
782
- maximum compatibility with consumers. Consumers SHOULD ignore the case of
783
- literal strings when parsing data.
784
-
785
- [RFC5234]: http://tools.ietf.org/html/rfc5234
786
-
787
-
788
- Entities
789
- ========
790
-
791
- Index
792
- -----
793
- The Index entity is a JSON object with site properties. The following properties
794
- are defined for the Index entity object.
795
-
796
- ### `name`
797
- The `name` field is a string with the site's name.
798
-
799
- ### `description`
800
- The `description` field is a string with the site's description.
801
-
802
- ### `URL`
803
- The `URL` field is a string with the URL to the site itself.
804
-
805
- ### `routes`
806
- The `routes` field is an object with keys as a route and the values as a route
807
- descriptor.
808
-
809
- The route is a string giving the URL template for the route, relative to the API
810
- root. The template contains URL parts separated by forward slashes, with each
811
- URL part either a static string, or a route variable encased in angle brackets.
812
-
813
- route = ( "/"
814
- / *( "/" ( token / route-variable ) ) )
815
- route-variable = "<" token ">"
816
-
817
- These routes can be converted into URLs by replacing all route variables with
818
- their relevant values, then concatenating the relative URL to the API base.
819
-
820
- The route descriptor is an object with the following defined properties.
821
-
822
- * `supports`: A JSON array of supported HTTP methods (verbs). Possible values
823
- are "HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"
824
- * `accepts_json`: A boolean indicating whether data can be passed directly via a
825
- POST request body. Default for missing properties is false.
826
- * `meta`: An Entity Meta entity. Typical `links` values consist of a `self` link
827
- pointing to the route's full URL.
828
-
829
- ### `meta`
830
- The `meta` field is a Entity Meta entity with metadata relating to the entity
831
- representation.
832
-
833
- Typical `links` values for the meta object consist of a `help` key with the
834
- value indicating a human-readable documentation page about the API.
835
-
836
- ### Example
837
-
838
- {
839
- "name": "My WordPress Site",
840
- "description": "Just another WordPress site",
841
- "URL": "http:\/\/example.com",
842
- "routes": {
843
- "\/": {
844
- "supports": [
845
- "HEAD",
846
- "GET"
847
- ],
848
- "meta": {
849
- "self": "http:\/\/example.com\/wp-json\/"
850
- }
851
- },
852
- "\/posts": {
853
- "supports": [
854
- "HEAD",
855
- "GET",
856
- "POST"
857
- ],
858
- "meta": {
859
- "self": "http:\/\/example.com\/wp-json\/posts"
860
- },
861
- "accepts_json": true
862
- },
863
- "\/posts\/<id>": {
864
- "supports": [
865
- "HEAD",
866
- "GET",
867
- "POST",
868
- "PUT",
869
- "PATCH",
870
- "DELETE"
871
- ],
872
- "accepts_json": true
873
- },
874
- "\/posts\/<id>\/revisions": {
875
- "supports": [
876
- "HEAD",
877
- "GET"
878
- ]
879
- },
880
- "\/posts\/<id>\/comments": {
881
- "supports": [
882
- "HEAD",
883
- "GET",
884
- "POST"
885
- ],
886
- "accepts_json": true
887
- },
888
- "\/posts\/<id>\/comments\/<comment>": {
889
- "supports": [
890
- "HEAD",
891
- "GET",
892
- "POST",
893
- "PUT",
894
- "PATCH",
895
- "DELETE"
896
- ],
897
- "accepts_json": true
898
- },
899
- },
900
- "meta": {
901
- "links": {
902
- "help": "https:\/\/github.com\/WP-API\/WP-API",
903
- "profile": "https:\/\/raw.github.com\/WP-API\/WP-API\/master\/docs\/schema.json"
904
- }
905
- }
906
- }
907
-
908
- Post
909
- ----
910
- The Post entity is a JSON object of post properties. Unless otherwise defined,
911
- properties are available in all contexts. The following properties are defined
912
- for the Post entity object:
913
-
914
- ### `title`
915
- The `title` field is a string with the post's title.
916
-
917
- ### `date`, `date_gmt`
918
- The `date` and `date_gmt` fields are strings with the post's creation date and
919
- time in the local time and UTC respectively. These fields follow the [RFC3339][]
920
- Section 5.6 datetime representation.
921
-
922
- date = date-time
923
- date_gmt = date-time
924
-
925
- [RFC3339]: http://tools.ietf.org/html/rfc3339
926
-
927
- ### `modified`, `modified_gmt`
928
- The `modified` and `modified_gmt` fields are strings with the post's last
929
- modification date and time in the local time and UTC respectively. These fields
930
- follow the [RFC3339][] Section 5.6 datetime representation.
931
-
932
- modified = date-time
933
- modified_gmt = date-time
934
-
935
- ### `date_tz`, `modified_tz`
936
- The `date_tz` and `modified_tz` fields are strings with the timezone applying to
937
- the `date` and `modified` fields respectively. The timezone is a [Olsen zoneinfo
938
- database][] identifier. While the `date` and `modified` fields include timezone
939
- offset information, the `date_tz` and `modified_tz` fields allow proper data
940
- operations across Daylight Savings Time boundaries.
941
-
942
- Note that in addition to the normal Olsen timezones, manual offsets may be
943
- given. These manual offsets use the deprecated `Etc/GMT+...` zones and specify
944
- an integer offset in hours from UTC.
945
-
946
- timezone = Olsen-timezone / manual-offset
947
- manual-offset = "Etc/GMT" ("-" / "+") 1*2( DIGIT )
948
-
949
- Consumers SHOULD use the fields if they perform mathematical operations on the
950
- `date` and `modified` fields (such as adding an hour to the last modification
951
- date) rather than relying on the `time-offset` in the `date` or
952
- `modified` fields.
953
-
954
- [Olsen zoneinfo database]: https://en.wikipedia.org/wiki/Tz_database
955
-
956
- ### `status`
957
- The `status` field is a string with the post's status. This status relates to
958
- where the post is in the editorial process. These are usually set values, but
959
- some providers may have extra post statuses.
960
-
961
- post-status = "draft" / "pending" / "private" / "publish" / "trash" / token
962
-
963
- Consumers who encounter an unknown or missing post status SHOULD treat it the
964
- same as a "draft" status.
965
-
966
- ### `type`
967
- The `type` field is a string with the post's type. This field is specific to
968
- providers, with the most basic representation being "post". The type of the
969
- post usually relates to the fields in the Post entity, with other types having
970
- additional fields specific to the type.
971
-
972
- post-type = "post" / token
973
-
974
- Consumers who encounter an unknown or missing post type SHOULD treat it the same
975
- as a "post" type.
976
-
977
- ### `name`
978
- The `name` field is a string with the post's slug.
979
-
980
- ### `author`
981
- The `author` field is a User entity with the user who created the post.
982
-
983
- ### `password`
984
- The `password` field is a string with the post's password. A zero-length
985
- password indicates that the post does not have a password.
986
-
987
- Consumers who encounter a missing password MUST treat it the same as a
988
- zero-length password.
989
-
990
- ### `content`
991
- The `content` field is a string with the post's content.
992
-
993
- ### `excerpt`
994
- The `excerpt` field is a string with the post's excerpt. This is usually a
995
- shortened version of the post content, suitable for displaying in
996
- collection views.
997
-
998
- Consumers who encounter a missing excerpt MAY present a shortened version of the
999
- `content` field instead.
1000
-
1001
- ### `content_raw`, `excerpt_raw`
1002
- The `content_raw` and `excerpt_raw` fields are strings with the post's content
1003
- and excerpt respectively. Unlike the `content` and `excerpt` fields, the value
1004
- has not been passed through internal filtering, and is suitable for editing.
1005
-
1006
- (Context Availability: `edit`)
1007
-
1008
- ### `parent`
1009
- The `parent` field is an integer or JSON object with the post's parent
1010
- post ID. A literal zero indicates that the post does not have a parent
1011
- post.
1012
-
1013
- post-parent = "0" / 1*DIGIT
1014
-
1015
- Consumers who encounter a missing parent ID MUST treat it the same as a parent
1016
- post ID of 0.
1017
-
1018
- Parent fields will be expanded into a full Post entity in the `view` or `edit`
1019
- contexts, but only one level deep. The embedded Post entity will be rendered
1020
- using the `parent` context.
1021
-
1022
- In the `parent` context, the field will contain an integer with the post's
1023
- parent post ID as above.
1024
-
1025
- ### `link`
1026
- The `link` field is a string with the full URL to the post's canonical view.
1027
- This is typically the human-readable location of the entity.
1028
-
1029
- ### `guid`
1030
- The `guid` field is a string with the post's globally unique identifier (GUID).
1031
-
1032
- The GUID is typically in URL form, as this is a relatively easy way of ensuring
1033
- that the GUID is globally unique. However, consumers MUST NOT treat the GUID as
1034
- a URL, and MUST treat the GUID as a string of arbitrary characters.
1035
-
1036
- ### `menu_order`
1037
- The `menu_order` field is an integer with the post's sorting position. This is
1038
- typically used to affect sorting when displaying the post in menus or lists.
1039
- Larger integers should be treated as sorting before smaller integers.
1040
-
1041
- menu-order = 1*DIGIT / "-" 1*DIGIT
1042
-
1043
- Consumers who encounter a missing sorting position MUST treat it the same as a
1044
- sorting position of 0.
1045
-
1046
- ### `comment_status`
1047
- The `comment_status` field is a string with the post's current commenting
1048
- status. This field indicates whether users can submit comments to the post.
1049
-
1050
- post-comment-status = "open" / "closed" / token
1051
-
1052
- Providers MAY use statuses other than "open" or "closed" to indicate other
1053
- statuses. Consumers who encounter an unknown or missing comment status SHOULD
1054
- treat it as "closed".
1055
-
1056
- ### `ping_status`
1057
- The `ping_status` field is a string with the post's current pingback/trackback
1058
- status. This field indicates whether users can submit pingbacks or trackbacks
1059
- to the post.
1060
-
1061
- ping-status = "open" / "closed" / token
1062
-
1063
- Providers MAY use statuses other than "open" or "closed" to indicate other
1064
- statuses. Consumers who encounter an unknown or missing ping status SHOULD treat
1065
- it as "closed".
1066
-
1067
- ### `sticky`
1068
- The `sticky` field is a boolean indicating whether the post is marked as a
1069
- sticky post. Consumers typically display sticky posts before other posts in
1070
- collection views.
1071
-
1072
- ### `post_thumbnail`
1073
- The `post_thumbnail` field is a Media entity.
1074
-
1075
- ### `post_format`
1076
- The `post_format` field is a string with the post format. The post format
1077
- indicates how some meta fields should be displayed. For example, posts with the
1078
- "link" format may wish to display an extra link to a URL specified in a meta
1079
- field or emphasise a link in the post content.
1080
-
1081
- post-format = "standard" / "aside" / "gallery" / "image" / "link" / "status" / "quote" / "video" / "audio" / "chat"
1082
-
1083
- Providers MUST NOT use post formats not specified by this specification, unless
1084
- specified in a subsequent version of the specification. Consumers MUST treat
1085
- unknown post formats as "standard".
1086
-
1087
- ### `terms`
1088
- The `terms` field is a Term collection.
1089
-
1090
- ### `post_meta`
1091
- The `meta` field is a Metadata entity with metadata relating to the post.
1092
-
1093
- ### `meta`
1094
- The `meta` field is a Entity Meta entity with metadata relating to the entity
1095
- representation.
1096
-
1097
- ### Example
1098
-
1099
- {
1100
- "ID": 1,
1101
- "title": "Hello world!q",
1102
- "status": "publish",
1103
- "type": "post",
1104
- "author": {
1105
- "ID": 1,
1106
- "name": "admin",
1107
- "slug": "admin",
1108
- "URL": "",
1109
- "avatar": "http:\/\/0.gravatar.com\/avatar\/c57c8945079831fa3c19caef02e44614&d=404&r=G",
1110
- "meta": {
1111
- "links": {
1112
- "self": "http:\/\/example.com\/wp-json\/users\/1",
1113
- "archives": "http:\/\/example.com\/wp-json\/users\/1\/posts"
1114
- }
1115
- },
1116
- "first_name": "",
1117
- "last_name": ""
1118
- },
1119
- "content": "<p>Welcome to WordPress. This is your first post. Edit or delete it, then start blogging!<\/p>\n",
1120
- "parent": 0,
1121
- "link": "http:\/\/example.com\/2013\/06\/02\/hello-world\/",
1122
- "date": "2013-06-02T05:28:00+10:00",
1123
- "modified": "2013-06-30T13:56:57+10:00",
1124
- "format": "standard",
1125
- "slug": "hello-world",
1126
- "guid": "http:\/\/example.com\/?p=1",
1127
- "excerpt": "",
1128
- "menu_order": 0,
1129
- "comment_status": "open",
1130
- "ping_status": "open",
1131
- "sticky": false,
1132
- "date_tz": "Australia\/Brisbane",
1133
- "date_gmt": "2013-06-02T05:28:00+00:00",
1134
- "modified_tz": "Australia\/Brisbane",
1135
- "modified_gmt": "2013-06-30T03:56:57+00:00",
1136
- "password": "",
1137
- "post_meta": [
1138
- ],
1139
- "meta": {
1140
- "links": {
1141
- "self": "http:\/\/example.com\/wp-json\/posts\/1",
1142
- "author": "http:\/\/example.com\/wp-json\/users\/1",
1143
- "collection": "http:\/\/example.com\/wp-json\/posts",
1144
- "replies": "http:\/\/example.com\/wp-json\/posts\/1\/comments",
1145
- "version-history": "http:\/\/example.com\/wp-json\/posts\/1\/revisions"
1146
- }
1147
- },
1148
- "featured_image": null,
1149
- "terms": {
1150
- "category": {
1151
- "ID": 1,
1152
- "name": "Uncategorized",
1153
- "slug": "uncategorized",
1154
- "parent": null,
1155
- "count": 7,
1156
- "meta": {
1157
- "links": {
1158
- "collection": "http:\/\/example.com\/wp-json\/taxonomies\/category\/terms",
1159
- "self": "http:\/\/example.com\/wp-json\/taxonomies\/category\/terms\/1"
1160
- }
1161
- }
1162
- }
1163
- }
1164
- }
1165
-
1166
-
1167
-
1168
- Entity Meta
1169
- -----------
1170
- The Entity Meta entity is a JSON object with custom metadata relating to the
1171
- representation of the parent entity.
1172
-
1173
- The following properties are defined for the Entity Meta entity object:
1174
-
1175
- ### `links`
1176
- The `links` field is a JSON object with hyperlinks to related entities. Each
1177
- item's key is a link relation as per the [IANA Link Relations registry][] with
1178
- the value of the item being the corresponding link URL.
1179
-
1180
- Typical link relations are:
1181
-
1182
- * `self`: A URL pointing to the current entity's location.
1183
- * `up`: A URL pointing to the parent entity's location.
1184
- * `collection`: A URL pointing to a collection that the entity is a member of.
1185
-
1186
- [IANA Link Relations registry]: http://www.iana.org/assignments/link-relations/link-relations.xml
1187
-
1188
-
1189
- User
1190
- ----
1191
- The User entity is a JSON object with user properties. The following properties
1192
- are defined for the User entity object:
1193
-
1194
- ### `ID`
1195
- The `ID` field is an integer with the user's ID.
1196
-
1197
- ### `name`
1198
- The `name` field is a string with the user's display name.
1199
-
1200
- ### `slug`
1201
- The `slug` field is a string with the user's slug.
1202
-
1203
- ### `URL`
1204
- The `URL` field is a string with the URL to the author's site. This is typically
1205
- an external link of the author's choice.
1206
-
1207
- ### `avatar`
1208
- The `avatar` field is a string with the URL to the author's avatar image.
1209
-
1210
- Providers SHOULD ensure that for users without an avatar image, this field is
1211
- either zero-length or the URL returns a HTTP 404 error code on access. Consumers
1212
- MAY display a default avatar instead of a zero-length or URL which returns
1213
- a HTTP 404 error code.
1214
-
1215
- ### `meta`
1216
- The `meta` field is a Entity Meta entity with metadata relating to the entity
1217
- representation.
1218
-
1219
-
1220
- Metadata
1221
- --------
1222
- The Metadata entity is a JSON array with metadata fields. Each metadata field is
1223
- a JSON object with `id`, `key` and `value` fields.
1224
-
1225
- ### `id`
1226
- The `id` field of the metadata field is a positive integer with the internal
1227
- metadata ID.
1228
-
1229
- ### `key`
1230
- The `key` field of the metadata field is a string with the metadata field name.
1231
-
1232
- ### `value`
1233
- The `value` field of the metadata field is a string with the metadata
1234
- field value.
1235
-
1236
-
1237
- Comment
1238
- -------
1239
- The Comment entity is a JSON object with comment properties. The following
1240
- properties are defined for the Comment entity object:
1241
-
1242
- ### `ID`
1243
- The `ID` field is an integer with the comment's ID.
1244
-
1245
- ### `content`
1246
- The `content` field is a string with the comment's content.
1247
-
1248
- ### `status`
1249
- The `status` field is a string with the comment's status. This field indicates
1250
- whether the comment is in the publishing process, or if it has been deleted or
1251
- marked as spam.
1252
-
1253
- comment-status = "hold" / "approved" / "spam" / "trash" / token
1254
-
1255
- Providers MAY use other values to indicate other statuses. Consumers who
1256
- encounter an unknown or missing status SHOULD treat it as "hold".
1257
-
1258
- ### `type`
1259
- The `type` field is a string with the comment's type. This is usually one of the
1260
- following, but providers may provide additional values.
1261
-
1262
- comment-type = "comment" / "trackback" / "pingback" / token
1263
-
1264
- Providers MAY use other values to indicate other types. Consumers who encounter
1265
- an unknown or missing status SHOULD treat it as "comment".
1266
-
1267
- ### `post`
1268
- The `post` field is an integer with the parent post for the comment, or a Post
1269
- entity describing the parent post. A literal zero indicates that the comment
1270
- does not have a parent post.
1271
-
1272
- comment-post-parent = "0" / 1*DIGIT
1273
-
1274
- Consumers who encounter a missing post ID MUST treat it the same as a parent
1275
- post ID of 0.
1276
-
1277
- ### `parent`
1278
- The `post` field is an integer with the parent comment, or a Comment entity
1279
- describing the parent comment. A literal zero indicates that the comment does
1280
- not have a parent comment.
1281
-
1282
- comment-parent = "0" / 1*DIGIT
1283
-
1284
- Consumers who encounter a missing parent ID MUST treat it the same as a parent
1285
- comment ID of 0.
1286
-
1287
- ### `author`
1288
- The `author` field is a User entity with the comment author's data, or a
1289
- User-like object for anonymous authors. The User-like object contains the
1290
- following properties:
1291
-
1292
- #### `ID`
1293
- The `ID` property on the User-like object is always set to `0` for anonymous
1294
- authors.
1295
-
1296
- #### `name`
1297
- The `name` property on the User-like object is a string with the author's name.
1298
-
1299
- #### `URL`
1300
- The `URL` property on the User-like object is a string with the author's URL.
1301
-
1302
- #### `avatar`
1303
- The `avatar` property on the User-like object is a string with the URL to the
1304
- author's avatar image.
1305
-
1306
- This property should be treated the same as the avatar property on the
1307
- User entity.
1308
-
1309
-
1310
- ### `date`, `date_gmt`
1311
- The `date` and `date_gmt` fields are strings with the post's creation date and
1312
- time in the local time and UTC respectively. These fields follow the [RFC3339][]
1313
- Section 5.6 datetime representation.
1314
-
1315
- date = date-time
1316
- date_gmt = date-time
1317
-
1318
- This field should be treated the same as the `date` and `date_gmt` properties on
1319
- a Post entity.
1320
-
1321
- [RFC3339]: http://tools.ietf.org/html/rfc3339
1322
-
1323
- ### `date_tz`, `modified_tz`
1324
- The `date_tz` and `modified_tz` fields are strings with the timezone applying to
1325
- the `date` and `modified` fields respectively. The timezone is a [Olsen zoneinfo
1326
- database][] identifier. While the `date` field includes timezone offset
1327
- information, the `date_tz` field allows proper data operations across Daylight
1328
- Savings Time boundaries.
1329
-
1330
- This field should be treated the same as the `date_tz` property on a
1331
- Post entity.
1332
-
1333
-
1334
- Media
1335
- -----
1336
- The Media entity is a JSON object based on the Post entity. It contains all
1337
- properties of the Post entity, with the following additional properties defined:
1338
-
1339
- ### `source`
1340
- The `source` field is a string with the URL of the entity's original file. For
1341
- image media, this is the source file that intermediate representations are
1342
- generated from. For non-image media, this is the attached media file itself.
1343
-
1344
- ### `is_image`
1345
- The `is_image` field is a boolean which indicates whether the entity's
1346
- associated file should be handled as an image.
1347
-
1348
- ### `attachment_meta`
1349
- The `attachment_meta` field is a Media Meta entity. If the file is not an image
1350
- (as indicated by the `is_image` field), this is an empty JSON object.
1351
-
1352
-
1353
- Media Meta
1354
- ----------
1355
- The Media Meta entity is a JSON object with properties relating to the
1356
- associated Media entity. The following properties are defined for the entity:
1357
-
1358
- ### `width`
1359
- The `width` field is an integer with the original file's width in pixels.
1360
-
1361
- ### `height`
1362
- The `height` field is an integer with the original file's height in pixels.
1363
-
1364
- ### `file`
1365
- The `file` field is a string with the path to the original file, relative to the
1366
- site's upload directory.
1367
-
1368
- ### `sizes`
1369
- The `sizes` field is a JSON object mapping intermediate image sizes to image
1370
- data objects. The key of each item is the size of the intermediate image as an
1371
- internal string representation. The value of each item has the following
1372
- properties defined.
1373
-
1374
- * `file`: The filename of the intermediate file, relative to the directory of
1375
- the original file.
1376
- * `width`: The width of the intermediate file in pixels.
1377
- * `height`: The height of the intermediate file in pixels.
1378
- * `mime-type`: The MIME type of the intermediate file.
1379
- * `url`: The full URL to the intermediate file.
1380
-
1381
- ### `image_meta`
1382
- The `image_meta` field is a JSON object mapping image meta properties to their
1383
- values. This data is taken from the EXIF data on the original image. The
1384
- following properties are defined.
1385
-
1386
- * `aperture`: The aperture used to create the original image as a decimal number
1387
- (with two decimal places).
1388
- * `credit`: Credit for the original image.
1389
- * `camera`: The camera used to create the original image.
1390
- * `created_timestamp`: When the file was created, as a Unix timestamp.
1391
- * `copyright`: Copyright for the original image.
1392
- * `focal_length`: The focal length used to create the original image as a
1393
- decimal string.
1394
- * `iso`: The ISO used to create the original image.
1395
- * `shutter_speed`: The shutter speed used to create the original image, as a
1396
- decimal string.
1397
- * `title`: The original title of the image.
1398
-
1399
-
1400
- Documents
1401
- =========
1402
-
1403
- Index
1404
- -----
1405
- The Index document is the root endpoint for the API server and describes the
1406
- contents and abilities of the API server.
1407
-
1408
- ### Body
1409
- The body of an Index document is an Index entity.
1410
-
1411
- ### Example
1412
-
1413
- {
1414
- "name":"My WordPress Site",
1415
- "description":"Just another WordPress site",
1416
- "URL":"http:\/\/example.com",
1417
- "routes": {
1418
- "\/": {
1419
- "supports": [ "HEAD", "GET" ]
1420
- },
1421
- "\/posts": {
1422
- "supports": [ "HEAD", "GET", "POST" ],
1423
- "accepts_json": true
1424
- },
1425
- "\/posts\/<id>": {
1426
- "supports": [ "HEAD", "GET", "POST", "PUT", "PATCH", "DELETE" ]
1427
- },
1428
- "\/posts\/<id>\/revisions": {
1429
- "supports": [ "HEAD", "GET" ]
1430
- },
1431
- "\/posts\/<id>\/comments": {
1432
- "supports": [ "HEAD", "GET", "POST" ],
1433
- "accepts_json":true
1434
- }
1435
- },
1436
- "meta": {
1437
- "links": {
1438
- "help":"http:\/\/codex.wordpress.org\/JSON_API"
1439
- }
1440
- }
1441
- }
1442
-
1443
-
1444
- Post
1445
- ----
1446
- A Post document is defined as the representation of a post item, analogous to an
1447
- Atom item.
1448
-
1449
- ### Headers
1450
- The following headers are sent when a Post is the main entity:
1451
-
1452
- * `Link`:
1453
- * `rel="alternate"; type=text/html`: The permalink for the Post
1454
- * `rel="collection"`: The endpoint of the Post Collection the Post is
1455
- contained in
1456
- * `rel="replies"`: The endpoint of the associated Comment Collection
1457
- * `rel="version-history"`: The endpoint of the Post Collection containing
1458
- the revisions of the Post
1459
-
1460
-
1461
- ### Body
1462
- The body of a Post document is a Post entity.
1463
-
1464
-
1465
- ### Example
1466
-
1467
- HTTP/1.1 200 OK
1468
- Date: Mon, 07 Jan 2013 03:35:14 GMT
1469
- Last-Modified: Mon, 07 Jan 2013 03:35:14 GMT
1470
- Link: <http://localhost/wptrunk/?p=1>; rel="alternate"; type=text/html
1471
- Link: <http://localhost/wptrunk/wp-json/users/1>; rel="author"
1472
- Link: <http://localhost/wptrunk/wp-json/posts>; rel="collection"
1473
- Link: <http://localhost/wptrunk/wp-json/posts/158/comments>; rel="replies"
1474
- Link: <http://localhost/wptrunk/wp-json/posts/158/revisions>; rel="version-history"
1475
- Content-Type: application/json; charset=UTF-8
1476
-
1477
- {
1478
- "ID":158,
1479
- "title":"This is a test!",
1480
- "status":"publish",
1481
- "type":"post",
1482
- "author":{
1483
- "ID":1,
1484
- "name":"admin",
1485
- "slug":"admin",
1486
- "URL":"",
1487
- "avatar":"http:\/\/0.gravatar.com\/avatar\/c57c8945079831fa3c19caef02e44614&d=404&r=G",
1488
- "meta":{
1489
- "links":{
1490
- "self":"http:\/\/localhost\/wptrunk\/wp-json\/users\/1",
1491
- "archives":"http:\/\/localhost\/wptrunk\/wp-json\/users\/1\/posts"
1492
- }
1493
- }
1494
- },
1495
- "content":"Hello.\r\n\r\nHah.",
1496
- "parent":0,
1497
- "link":"http:\/\/localhost\/wptrunk\/158\/this-is-a-test\/",
1498
- "date":"2013-01-07T13:35:14+10:00",
1499
- "modified":"2013-01-07T13:49:40+10:00",
1500
- "format":"standard",
1501
- "slug":"this-is-a-test",
1502
- "guid":"http:\/\/localhost\/wptrunk\/?p=158",
1503
- "excerpt":"",
1504
- "menu_order":0,
1505
- "comment_status":"open",
1506
- "ping_status":"open",
1507
- "sticky":false,
1508
- "date_tz":"Australia\/Brisbane",
1509
- "date_gmt":"2013-01-07T03:35:14+00:00",
1510
- "modified_tz":"Australia\/Brisbane",
1511
- "modified_gmt":"2013-01-07T03:49:40+00:00",
1512
- "post_thumbnail":[],
1513
- "terms":{
1514
- "category":{
1515
- "ID":1,
1516
- "name":"Uncategorized",
1517
- "slug":"uncategorized",
1518
- "group":0,
1519
- "parent":0,
1520
- "count":4,
1521
- "meta":{
1522
- "links":{
1523
- "collection":"http:\/\/localhost\/wptrunk\/wp-json\/taxonomy\/category",
1524
- "self":"http:\/\/localhost\/wptrunk\/wp-json\/taxonomy\/category\/terms\/1"
1525
- }
1526
- }
1527
- }
1528
- },
1529
- "post_meta":[],
1530
- "meta":{
1531
- "links":{
1532
- "self":"http:\/\/localhost\/wptrunk\/wp-json\/posts\/158",
1533
- "author":"http:\/\/localhost\/wptrunk\/wp-json\/users\/1",
1534
- "collection":"http:\/\/localhost\/wptrunk\/wp-json\/posts",
1535
- "replies":"http:\/\/localhost\/wptrunk\/wp-json\/posts\/158\/comments",
1536
- "version-history":"http:\/\/localhost\/wptrunk\/wp-json\/posts\/158\/revisions"
1537
- }
1538
- }
1539
- }
1540
-
1541
-
1542
- Post Collection
1543
- ---------------
1544
- A Post Collection document is defined as a collection of Post entities.
1545
-
1546
- ### Headers
1547
- The following headers are sent when a Post Collection is the main entity:
1548
-
1549
- * `Link`:
1550
- * `rel="item"` - Each item in the collection has a corresponding Link header
1551
- containing the location of the endpoint for that resource.
1552
-
1553
-
1554
- ### Body
1555
- The Post Collection document is a JSON array of Post entities.
1556
-
1557
-
1558
- User
1559
- ----
1560
- The User document describes a member of the site.
1561
-
1562
- ### Body
1563
- The body of a User document is a User entity.
1564
-
1565
-
1566
- Appendix A: JSON Schema
1567
- =======================
1568
- The JSON Schema describing the entities in this document is available in
1569
- schema.json.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extras.php CHANGED
@@ -1,30 +1,20 @@
1
  <?php
2
-
3
  /**
4
- * Extra File where a lot of the extra functions from plugin.php go.
 
 
5
  *
6
  * @package WordPress
7
  * @subpackage JSON API
8
- *
9
- * @TODO fix this doc block (Make it better maybe?)
10
  */
11
 
12
  add_action( 'wp_enqueue_scripts', 'rest_register_scripts', -100 );
13
  add_action( 'admin_enqueue_scripts', 'rest_register_scripts', -100 );
14
- add_action( 'xmlrpc_rsd_apis', 'rest_output_rsd' );
15
- add_action( 'wp_head', 'rest_output_link_wp_head', 10, 0 );
16
- add_action( 'template_redirect', 'rest_output_link_header', 11, 0 );
17
- add_action( 'auth_cookie_malformed', 'rest_cookie_collect_status' );
18
- add_action( 'auth_cookie_expired', 'rest_cookie_collect_status' );
19
- add_action( 'auth_cookie_bad_username', 'rest_cookie_collect_status' );
20
- add_action( 'auth_cookie_bad_hash', 'rest_cookie_collect_status' );
21
- add_action( 'auth_cookie_valid', 'rest_cookie_collect_status' );
22
- add_filter( 'rest_authentication_errors', 'rest_cookie_check_errors', 100 );
23
-
24
-
25
 
26
  /**
27
- * Register API Javascript helpers.
 
 
28
  *
29
  * @see wp_register_scripts()
30
  */
@@ -36,134 +26,14 @@ function rest_register_scripts() {
36
  }
37
 
38
  /**
39
- * Add the API URL to the WP RSD endpoint.
40
- */
41
- function rest_output_rsd() {
42
- $api_root = get_rest_url();
43
-
44
- if ( empty( $api_root ) ) {
45
- return;
46
- }
47
- ?>
48
- <api name="WP-API" blogID="1" preferred="false" apiLink="<?php echo esc_url( $api_root ); ?>" />
49
- <?php
50
- }
51
-
52
- /**
53
- * Output API link tag into page header.
54
- *
55
- * @see get_rest_url()
56
- */
57
- function rest_output_link_wp_head() {
58
- $api_root = get_rest_url();
59
-
60
- if ( empty( $api_root ) ) {
61
- return;
62
- }
63
-
64
- echo "<link rel='https://github.com/WP-API/WP-API' href='" . esc_url( $api_root ) . "' />\n";
65
- }
66
-
67
- /**
68
- * Send a Link header for the API.
69
- */
70
- function rest_output_link_header() {
71
- if ( headers_sent() ) {
72
- return;
73
- }
74
-
75
- $api_root = get_rest_url();
76
-
77
- if ( empty($api_root) ) {
78
- return;
79
- }
80
-
81
- header( 'Link: <' . esc_url_raw( $api_root ) . '>; rel="https://github.com/WP-API/WP-API"', false );
82
- }
83
-
84
- /**
85
- * Check for errors when using cookie-based authentication.
86
- *
87
- * WordPress' built-in cookie authentication is always active
88
- * for logged in users. However, the API has to check nonces
89
- * for each request to ensure users are not vulnerable to CSRF.
90
- *
91
- * @global mixed $wp_rest_auth_cookie
92
- *
93
- * @param WP_Error|mixed $result Error from another authentication handler,
94
- * null if we should handle it, or another
95
- * value if not
96
- * @return WP_Error|mixed|bool WP_Error if the cookie is invalid, the $result,
97
- * otherwise true.
98
- */
99
- function rest_cookie_check_errors( $result ) {
100
- if ( ! empty( $result ) ) {
101
- return $result;
102
- }
103
-
104
- global $wp_rest_auth_cookie;
105
-
106
- /*
107
- * Is cookie authentication being used? (If we get an auth
108
- * error, but we're still logged in, another authentication
109
- * must have been used.)
110
- */
111
- if ( true !== $wp_rest_auth_cookie && is_user_logged_in() ) {
112
- return $result;
113
- }
114
-
115
- // Is there a nonce?
116
- $nonce = null;
117
- if ( isset( $_REQUEST['_wp_rest_nonce'] ) ) {
118
- $nonce = $_REQUEST['_wp_rest_nonce'];
119
- } elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) {
120
- $nonce = $_SERVER['HTTP_X_WP_NONCE'];
121
- }
122
-
123
- if ( null === $nonce ) {
124
- // No nonce at all, so act as if it's an unauthenticated request.
125
- wp_set_current_user( 0 );
126
- return true;
127
- }
128
-
129
- // Check the nonce.
130
- $result = wp_verify_nonce( $nonce, 'wp_rest' );
131
- if ( ! $result ) {
132
- return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) );
133
- }
134
-
135
- return true;
136
- }
137
-
138
- /**
139
- * Collect cookie authentication status.
140
  *
141
- * Collects errors from {@see wp_validate_auth_cookie} for
142
- * use by {@see rest_cookie_check_errors}.
143
  *
144
- * @see current_action()
145
- * @global mixed $wp_rest_auth_cookie
146
- */
147
- function rest_cookie_collect_status() {
148
- global $wp_rest_auth_cookie;
149
-
150
- $status_type = current_action();
151
-
152
- if ( 'auth_cookie_valid' !== $status_type ) {
153
- $wp_rest_auth_cookie = substr( $status_type, 12 );
154
- return;
155
- }
156
-
157
- $wp_rest_auth_cookie = true;
158
- }
159
-
160
- /**
161
- * Retrieve the avatar urls in various sizes based on a given email address.
162
- *
163
- * {@see get_avatar_url()}
164
  *
165
  * @param string $email Email address.
166
- * @return array $urls Gravatar url for each size.
167
  */
168
  function rest_get_avatar_urls( $email ) {
169
  $avatar_sizes = rest_get_avatar_sizes();
@@ -177,120 +47,47 @@ function rest_get_avatar_urls( $email ) {
177
  }
178
 
179
  /**
180
- * Return the pixel sizes for avatars.
 
 
181
  *
182
- * @return array
183
  */
184
  function rest_get_avatar_sizes() {
 
 
 
 
 
 
 
 
 
 
 
185
  return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
186
  }
187
 
188
  /**
189
- * Parse an RFC3339 timestamp into a DateTime.
190
  *
191
- * @param string $date RFC3339 timestamp.
192
- * @param bool $force_utc Force UTC timezone instead of using the timestamp's TZ.
193
- * @return DateTime DateTime instance.
194
- */
195
- function rest_parse_date( $date, $force_utc = false ) {
196
- if ( $force_utc ) {
197
- $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date );
198
- }
199
-
200
- $regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#';
201
-
202
- if ( ! preg_match( $regex, $date, $matches ) ) {
203
- return false;
204
- }
205
-
206
- return strtotime( $date );
207
- }
208
-
209
- /**
210
- * Get a local date with its GMT equivalent, in MySQL datetime format.
211
  *
212
- * @param string $date RFC3339 timestamp
213
- * @param bool $force_utc Whether a UTC timestamp should be forced.
214
- * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s),
215
- * null on failure.
216
- */
217
- function rest_get_date_with_gmt( $date, $force_utc = false ) {
218
- $date = rest_parse_date( $date, $force_utc );
219
-
220
- if ( empty( $date ) ) {
221
- return null;
222
- }
223
-
224
- $utc = date( 'Y-m-d H:i:s', $date );
225
- $local = get_date_from_gmt( $utc );
226
-
227
- return array( $local, $utc );
228
- }
229
-
230
- /**
231
- * Parses and formats a MySQL datetime (Y-m-d H:i:s) for ISO8601/RFC3339
232
- *
233
- * Explicitly strips timezones, as datetimes are not saved with any timezone
234
- * information. Including any information on the offset could be misleading.
235
- *
236
- * @param string $date
237
- */
238
- function rest_mysql_to_rfc3339( $date_string ) {
239
- $formatted = mysql2date( 'c', $date_string, false );
240
-
241
- // Strip timezone information
242
- return preg_replace( '/(?:Z|[+-]\d{2}(?::\d{2})?)$/', '', $formatted );
243
- }
244
-
245
-
246
- /**
247
- * Get the timezone object for the site.
248
- *
249
- * @return DateTimeZone DateTimeZone instance.
250
- */
251
- function rest_get_timezone() {
252
- static $zone = null;
253
-
254
- if ( null !== $zone ) {
255
- return $zone;
256
- }
257
-
258
- $tzstring = get_option( 'timezone_string' );
259
-
260
- if ( ! $tzstring ) {
261
- // Create a UTC+- zone if no timezone string exists
262
- $current_offset = get_option( 'gmt_offset' );
263
- if ( 0 === $current_offset ) {
264
- $tzstring = 'UTC';
265
- } elseif ( $current_offset < 0 ) {
266
- $tzstring = 'Etc/GMT' . $current_offset;
267
- } else {
268
- $tzstring = 'Etc/GMT+' . $current_offset;
269
- }
270
- }
271
- $zone = new DateTimeZone( $tzstring );
272
-
273
- return $zone;
274
- }
275
-
276
- /**
277
- * Retrieve the avatar url for a user who provided a user ID or email address.
278
- *
279
- * @deprecated WPAPI-2.0
280
- * {@see get_avatar()} doesn't return just the URL, so we have to
281
- * extract it here.
282
  *
283
  * @param string $email Email address.
284
  * @return string URL for the user's avatar, empty string otherwise.
285
  */
286
  function rest_get_avatar_url( $email ) {
287
  _deprecated_function( 'rest_get_avatar_url', 'WPAPI-2.0', 'rest_get_avatar_urls' );
288
- /**
289
- * Use the WP Core `get_avatar_url()` function introduced in 4.2.
290
- */
291
  if ( function_exists( 'get_avatar_url' ) ) {
292
  return esc_url_raw( get_avatar_url( $email ) );
293
  }
 
294
  $avatar_html = get_avatar( $email );
295
 
296
  // Strip the avatar url from the get_avatar img tag.
@@ -302,3 +99,39 @@ function rest_get_avatar_url( $email ) {
302
 
303
  return '';
304
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <?php
 
2
  /**
3
+ * Extra File
4
+ *
5
+ * Contains extra functions from plugin.php go.
6
  *
7
  * @package WordPress
8
  * @subpackage JSON API
 
 
9
  */
10
 
11
  add_action( 'wp_enqueue_scripts', 'rest_register_scripts', -100 );
12
  add_action( 'admin_enqueue_scripts', 'rest_register_scripts', -100 );
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  /**
15
+ * Registers REST API JavaScript helpers.
16
+ *
17
+ * @since 4.4.0
18
  *
19
  * @see wp_register_scripts()
20
  */
26
  }
27
 
28
  /**
29
+ * Retrieves the avatar urls in various sizes based on a given email address.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  *
31
+ * @since 4.4.0
 
32
  *
33
+ * @see get_avatar_url()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  *
35
  * @param string $email Email address.
36
+ * @return array $urls Gravatar url for each size.
37
  */
38
  function rest_get_avatar_urls( $email ) {
39
  $avatar_sizes = rest_get_avatar_sizes();
47
  }
48
 
49
  /**
50
+ * Retrieves the pixel sizes for avatars.
51
+ *
52
+ * @since 4.4.0
53
  *
54
+ * @return array List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`.
55
  */
56
  function rest_get_avatar_sizes() {
57
+ /**
58
+ * Filter the REST avatar sizes.
59
+ *
60
+ * Use this filter to adjust the array of sizes returned by the
61
+ * `rest_get_avatar_sizes` function.
62
+ *
63
+ * @since 4.4.0
64
+ *
65
+ * @param array $sizes An array of int values that are the pixel sizes for avatars.
66
+ * Default `[ 24, 48, 96 ]`.
67
+ */
68
  return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
69
  }
70
 
71
  /**
72
+ * Retrieves the avatar url for a user who provided a user ID or email address.
73
  *
74
+ * get_avatar() doesn't return just the URL, so we have to extract it here.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  *
76
+ * @since 4.4.0
77
+ * @deprecated WPAPI-2.0 rest_get_avatar_urls()
78
+ * @see rest_get_avatar_urls()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  *
80
  * @param string $email Email address.
81
  * @return string URL for the user's avatar, empty string otherwise.
82
  */
83
  function rest_get_avatar_url( $email ) {
84
  _deprecated_function( 'rest_get_avatar_url', 'WPAPI-2.0', 'rest_get_avatar_urls' );
85
+
86
+ // Use the WP Core `get_avatar_url()` function introduced in 4.2.
 
87
  if ( function_exists( 'get_avatar_url' ) ) {
88
  return esc_url_raw( get_avatar_url( $email ) );
89
  }
90
+
91
  $avatar_html = get_avatar( $email );
92
 
93
  // Strip the avatar url from the get_avatar img tag.
99
 
100
  return '';
101
  }
102
+
103
+ if ( ! function_exists( 'wp_is_numeric_array' ) ) {
104
+ /**
105
+ * Determines if the variable is a numeric-indexed array.
106
+ *
107
+ * @since 4.4.0
108
+ *
109
+ * @param mixed $data Variable to check.
110
+ * @return bool Whether the variable is a list.
111
+ */
112
+ function wp_is_numeric_array( $data ) {
113
+ if ( ! is_array( $data ) ) {
114
+ return false;
115
+ }
116
+
117
+ $keys = array_keys( $data );
118
+ $string_keys = array_filter( $keys, 'is_string' );
119
+ return count( $string_keys ) === 0;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Parses and formats a MySQL datetime (Y-m-d H:i:s) for ISO8601/RFC3339.
125
+ *
126
+ * Explicitly strips timezones, as datetimes are not saved with any timezone
127
+ * information. Including any information on the offset could be misleading.
128
+ *
129
+ * @deprecated WPAPI-2.0 mysql_to_rfc3339()
130
+ *
131
+ * @param string $date_string Date string to parse and format.
132
+ * @return string Date formatted for ISO8601/RFC3339.
133
+ */
134
+ function rest_mysql_to_rfc3339( $date_string ) {
135
+ _deprecated_function( 'rest_mysql_to_rfc3339', 'WPAPI-2.0', 'mysql_to_rfc3339' );
136
+ return mysql_to_rfc3339( $date_string );
137
+ }
lib/endpoints/class-wp-rest-attachments-controller.php CHANGED
@@ -21,7 +21,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
21
  $parent = get_post( (int) $request['post'] );
22
  $post_parent_type = get_post_type_object( $parent->post_type );
23
  if ( ! current_user_can( $post_parent_type->cap->edit_post, $request['post'] ) ) {
24
- return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => 401 ) );
25
  }
26
  }
27
 
@@ -71,18 +71,34 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
71
  return $id;
72
  }
73
 
 
 
 
74
  wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );
75
 
 
 
 
 
76
  $this->update_additional_fields_for_object( $attachment, $request );
77
 
78
- $response = $this->get_item( array(
79
- 'id' => $id,
80
- 'context' => 'edit',
81
- ) );
82
  $response = rest_ensure_response( $response );
83
  $response->set_status( 201 );
84
  $response->header( 'Location', rest_url( '/wp/v2/' . $this->get_post_type_base( $attachment->post_type ) . '/' . $id ) );
85
 
 
 
 
 
 
 
 
 
 
86
  return $response;
87
 
88
  }
@@ -103,16 +119,18 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
103
  $data = $response->get_data();
104
 
105
  if ( isset( $request['alt_text'] ) ) {
106
- update_post_meta( $data['id'], '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
107
  }
108
 
109
- $response = $this->get_item( array(
110
- 'id' => $data['id'],
111
- 'context' => 'edit',
112
- ));
113
- $response = rest_ensure_response( $response );
114
- $response->header( 'Location', rest_url( '/wp/v2/' . $this->get_post_type_base( $this->post_type ) . '/' . $data['id'] ) );
115
- return $response;
 
 
116
  }
117
 
118
  /**
@@ -125,15 +143,15 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
125
  $prepared_attachment = parent::prepare_item_for_database( $request );
126
 
127
  if ( isset( $request['caption'] ) ) {
128
- $prepared_attachment->post_excerpt = wp_filter_post_kses( $request['caption'] );
129
  }
130
 
131
  if ( isset( $request['description'] ) ) {
132
- $prepared_attachment->post_content = wp_filter_post_kses( $request['description'] );
133
  }
134
 
135
  if ( isset( $request['post'] ) ) {
136
- $prepared_attachment->post_parent = (int) $request['post_parent'];
137
  }
138
 
139
  return $prepared_attachment;
@@ -165,8 +183,30 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
165
  $img_url_basename = wp_basename( $data['source_url'] );
166
 
167
  foreach ( $data['media_details']['sizes'] as $size => &$size_data ) {
 
 
 
 
 
 
168
  // Use the same method image_downsize() does
169
- $size_data['source_url'] = str_replace( $img_url_basename, $size_data['file'], $data['source_url'] );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  }
171
  } else {
172
  $data['media_details']['sizes'] = new stdClass;
@@ -177,11 +217,20 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
177
  $data = $this->filter_response_by_context( $data, $context );
178
 
179
  // Wrap the data in a response object
180
- $data = rest_ensure_response( $data );
181
-
182
- $data->add_links( $this->prepare_links( $post ) );
183
-
184
- return apply_filters( 'rest_prepare_attachment', $data, $post, $request );
 
 
 
 
 
 
 
 
 
185
  }
186
 
187
  /**
@@ -194,40 +243,49 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
194
  $schema = parent::get_item_schema();
195
 
196
  $schema['properties']['alt_text'] = array(
197
- 'description' => 'Alternative text to display when attachment is not displayed.',
198
  'type' => 'string',
199
  'context' => array( 'view', 'edit', 'embed' ),
 
 
 
200
  );
201
  $schema['properties']['caption'] = array(
202
- 'description' => 'The caption for the attachment.',
203
  'type' => 'string',
204
  'context' => array( 'view', 'edit' ),
 
 
 
205
  );
206
  $schema['properties']['description'] = array(
207
- 'description' => 'The description for the attachment.',
208
  'type' => 'string',
209
  'context' => array( 'view', 'edit' ),
 
 
 
210
  );
211
  $schema['properties']['media_type'] = array(
212
- 'description' => 'Type of attachment.',
213
  'type' => 'string',
214
  'enum' => array( 'image', 'file' ),
215
  'context' => array( 'view', 'edit', 'embed' ),
216
  'readonly' => true,
217
  );
218
  $schema['properties']['media_details'] = array(
219
- 'description' => 'Details about the attachment file, specific to its type.',
220
  'type' => 'object',
221
- 'context' => array( 'view', 'edit' ),
222
  'readonly' => true,
223
  );
224
  $schema['properties']['post'] = array(
225
- 'description' => 'The ID for the associated post of the attachment.',
226
  'type' => 'integer',
227
  'context' => array( 'view', 'edit' ),
228
  );
229
  $schema['properties']['source_url'] = array(
230
- 'description' => 'URL to the original attachment file.',
231
  'type' => 'string',
232
  'format' => 'uri',
233
  'context' => array( 'view', 'edit', 'embed' ),
@@ -287,6 +345,9 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
287
  // Get the content-type
288
  $type = array_shift( $headers['content_type'] );
289
 
 
 
 
290
  // Save the file
291
  $tmpfname = wp_tempnam( $filename );
292
 
@@ -321,6 +382,22 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
321
  return $sideloaded;
322
  }
323
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  /**
325
  * Handle an upload via multipart/form-data ($_FILES)
326
  *
@@ -334,8 +411,9 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
334
  }
335
 
336
  // Verify hash, if given
337
- if ( ! empty( $headers['CONTENT_MD5'] ) ) {
338
- $expected = trim( $headers['CONTENT_MD5'] );
 
339
  $actual = md5_file( $files['file']['tmp_name'] );
340
  if ( $expected !== $actual ) {
341
  return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected' ), array( 'status' => 412 ) );
@@ -351,7 +429,9 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
351
  $overrides['action'] = 'wp_handle_mock_upload';
352
  }
353
 
354
- $file = wp_handle_upload( $files, $overrides );
 
 
355
 
356
  if ( isset( $file['error'] ) ) {
357
  return new WP_Error( 'rest_upload_unknown_error', $file['error'], array( 'status' => 500 ) );
21
  $parent = get_post( (int) $request['post'] );
22
  $post_parent_type = get_post_type_object( $parent->post_type );
23
  if ( ! current_user_can( $post_parent_type->cap->edit_post, $request['post'] ) ) {
24
+ return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => rest_authorization_required_code() ) );
25
  }
26
  }
27
 
71
  return $id;
72
  }
73
 
74
+ /** Include admin functions to get access to wp_generate_attachment_metadata() */
75
+ require_once ABSPATH . 'wp-admin/includes/admin.php';
76
+
77
  wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );
78
 
79
+ if ( isset( $request['alt_text'] ) ) {
80
+ update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
81
+ }
82
+
83
  $this->update_additional_fields_for_object( $attachment, $request );
84
 
85
+ $get_request = new WP_REST_Request;
86
+ $get_request->set_param( 'id', $id );
87
+ $get_request->set_param( 'context', 'edit' );
88
+ $response = $this->get_item( $get_request );
89
  $response = rest_ensure_response( $response );
90
  $response->set_status( 201 );
91
  $response->header( 'Location', rest_url( '/wp/v2/' . $this->get_post_type_base( $attachment->post_type ) . '/' . $id ) );
92
 
93
+ /**
94
+ * Fires after a single attachment is created or updated via the REST API.
95
+ *
96
+ * @param object $attachment Inserted attachment.
97
+ * @param WP_REST_Request $request The request sent to the API.
98
+ * @param bool $creating True when creating an attachment, false when updating.
99
+ */
100
+ do_action( 'rest_insert_attachment', $attachment, $request, true );
101
+
102
  return $response;
103
 
104
  }
119
  $data = $response->get_data();
120
 
121
  if ( isset( $request['alt_text'] ) ) {
122
+ update_post_meta( $data['id'], '_wp_attachment_image_alt', $request['alt_text'] );
123
  }
124
 
125
+ $get_request = new WP_REST_Request;
126
+ $get_request->set_param( 'id', $data['id'] );
127
+ $get_request->set_param( 'context', 'edit' );
128
+ $response = $this->get_item( $get_request );
129
+
130
+ /* This action is documented in lib/endpoints/class-wp-rest-attachments-controller.php */
131
+ do_action( 'rest_insert_attachment', $data, $request, false );
132
+
133
+ return rest_ensure_response( $response );
134
  }
135
 
136
  /**
143
  $prepared_attachment = parent::prepare_item_for_database( $request );
144
 
145
  if ( isset( $request['caption'] ) ) {
146
+ $prepared_attachment->post_excerpt = $request['caption'];
147
  }
148
 
149
  if ( isset( $request['description'] ) ) {
150
+ $prepared_attachment->post_content = $request['description'];
151
  }
152
 
153
  if ( isset( $request['post'] ) ) {
154
+ $prepared_attachment->post_parent = (int) $request['post'];
155
  }
156
 
157
  return $prepared_attachment;
183
  $img_url_basename = wp_basename( $data['source_url'] );
184
 
185
  foreach ( $data['media_details']['sizes'] as $size => &$size_data ) {
186
+
187
+ if ( isset( $size_data['mime-type'] ) ) {
188
+ $size_data['mime_type'] = $size_data['mime-type'];
189
+ unset( $size_data['mime-type'] );
190
+ }
191
+
192
  // Use the same method image_downsize() does
193
+ $image_src = wp_get_attachment_image_src( $post->ID, $size );
194
+ if ( ! $image_src ) {
195
+ continue;
196
+ }
197
+
198
+ $size_data['source_url'] = $image_src[0];
199
+ }
200
+
201
+ $full_src = wp_get_attachment_image_src( $post->ID, 'full' );
202
+ if ( ! empty( $full_src ) ) {
203
+ $data['media_details']['sizes']['full'] = array(
204
+ 'file' => wp_basename( $full_src[0] ),
205
+ 'width' => $full_src[1],
206
+ 'height' => $full_src[2],
207
+ 'mime_type' => $post->post_mime_type,
208
+ 'source_url' => $full_src[0],
209
+ );
210
  }
211
  } else {
212
  $data['media_details']['sizes'] = new stdClass;
217
  $data = $this->filter_response_by_context( $data, $context );
218
 
219
  // Wrap the data in a response object
220
+ $response = rest_ensure_response( $data );
221
+
222
+ $response->add_links( $this->prepare_links( $post ) );
223
+
224
+ /**
225
+ * Filter an attachment returned from the API.
226
+ *
227
+ * Allows modification of the attachment right before it is returned.
228
+ *
229
+ * @param WP_REST_Response $response The response object.
230
+ * @param WP_Post $post The original attachment post.
231
+ * @param WP_REST_Request $request Request used to generate the response.
232
+ */
233
+ return apply_filters( 'rest_prepare_attachment', $response, $post, $request );
234
  }
235
 
236
  /**
243
  $schema = parent::get_item_schema();
244
 
245
  $schema['properties']['alt_text'] = array(
246
+ 'description' => __( 'Alternative text to display when attachment is not displayed.' ),
247
  'type' => 'string',
248
  'context' => array( 'view', 'edit', 'embed' ),
249
+ 'arg_options' => array(
250
+ 'sanitize_callback' => 'sanitize_text_field',
251
+ ),
252
  );
253
  $schema['properties']['caption'] = array(
254
+ 'description' => __( 'The caption for the attachment.' ),
255
  'type' => 'string',
256
  'context' => array( 'view', 'edit' ),
257
+ 'arg_options' => array(
258
+ 'sanitize_callback' => 'wp_filter_post_kses',
259
+ ),
260
  );
261
  $schema['properties']['description'] = array(
262
+ 'description' => __( 'The description for the attachment.' ),
263
  'type' => 'string',
264
  'context' => array( 'view', 'edit' ),
265
+ 'arg_options' => array(
266
+ 'sanitize_callback' => 'wp_filter_post_kses',
267
+ ),
268
  );
269
  $schema['properties']['media_type'] = array(
270
+ 'description' => __( 'Type of attachment.' ),
271
  'type' => 'string',
272
  'enum' => array( 'image', 'file' ),
273
  'context' => array( 'view', 'edit', 'embed' ),
274
  'readonly' => true,
275
  );
276
  $schema['properties']['media_details'] = array(
277
+ 'description' => __( 'Details about the attachment file, specific to its type.' ),
278
  'type' => 'object',
279
+ 'context' => array( 'view', 'edit', 'embed' ),
280
  'readonly' => true,
281
  );
282
  $schema['properties']['post'] = array(
283
+ 'description' => __( 'The id for the associated post of the attachment.' ),
284
  'type' => 'integer',
285
  'context' => array( 'view', 'edit' ),
286
  );
287
  $schema['properties']['source_url'] = array(
288
+ 'description' => __( 'URL to the original attachment file.' ),
289
  'type' => 'string',
290
  'format' => 'uri',
291
  'context' => array( 'view', 'edit', 'embed' ),
345
  // Get the content-type
346
  $type = array_shift( $headers['content_type'] );
347
 
348
+ /** Include admin functions to get access to wp_tempnam() and wp_handle_sideload() */
349
+ require_once ABSPATH . 'wp-admin/includes/admin.php';
350
+
351
  // Save the file
352
  $tmpfname = wp_tempnam( $filename );
353
 
382
  return $sideloaded;
383
  }
384
 
385
+ /**
386
+ * Get the query params for collections of attachments.
387
+ *
388
+ * @return array
389
+ */
390
+ public function get_collection_params() {
391
+ $params = parent::get_collection_params();
392
+ $params['parent'] = array(
393
+ 'description' => __( 'Limit results to attachments from a specified parent.' ),
394
+ 'type' => 'integer',
395
+ 'default' => null,
396
+ 'sanitize_callback' => 'absint',
397
+ );
398
+ return $params;
399
+ }
400
+
401
  /**
402
  * Handle an upload via multipart/form-data ($_FILES)
403
  *
411
  }
412
 
413
  // Verify hash, if given
414
+ if ( ! empty( $headers['content_md5'] ) ) {
415
+ $content_md5 = array_shift( $headers['content_md5'] );
416
+ $expected = trim( $content_md5 );
417
  $actual = md5_file( $files['file']['tmp_name'] );
418
  if ( $expected !== $actual ) {
419
  return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected' ), array( 'status' => 412 ) );
429
  $overrides['action'] = 'wp_handle_mock_upload';
430
  }
431
 
432
+ /** Include admin functions to get access to wp_handle_upload() */
433
+ require_once ABSPATH . 'wp-admin/includes/admin.php';
434
+ $file = wp_handle_upload( $files['file'], $overrides );
435
 
436
  if ( isset( $file['error'] ) ) {
437
  return new WP_Error( 'rest_upload_unknown_error', $file['error'], array( 'status' => 500 ) );
lib/endpoints/class-wp-rest-comments-controller.php CHANGED
@@ -10,66 +10,21 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
10
  */
11
  public function register_routes() {
12
 
13
- $query_params = $this->get_collection_params();
14
  register_rest_route( 'wp/v2', '/comments', array(
15
  array(
16
  'methods' => WP_REST_Server::READABLE,
17
  'callback' => array( $this, 'get_items' ),
18
  'permission_callback' => array( $this, 'get_items_permissions_check' ),
19
- 'args' => $query_params,
20
  ),
21
  array(
22
  'methods' => WP_REST_Server::CREATABLE,
23
  'callback' => array( $this, 'create_item' ),
24
  'permission_callback' => array( $this, 'create_item_permissions_check' ),
25
- 'args' => array(
26
- 'post' => array(
27
- 'required' => true,
28
- 'sanitize_callback' => 'absint',
29
- ),
30
- 'type' => array(
31
- 'default' => '',
32
- 'sanitize_callback' => 'sanitize_key',
33
- ),
34
- 'parent' => array(
35
- 'default' => 0,
36
- 'sanitize_callback' => 'absint',
37
- ),
38
- 'content' => array(
39
- 'default' => '',
40
- 'sanitize_callback' => 'wp_filter_post_kses',
41
- ),
42
- 'author' => array(
43
- 'default' => 0,
44
- 'sanitize_callback' => 'absint',
45
- ),
46
- 'author_name' => array(
47
- 'default' => '',
48
- 'sanitize_callback' => 'sanitize_text_field',
49
- ),
50
- 'author_email' => array(
51
- 'default' => '',
52
- 'sanitize_callback' => 'sanitize_email',
53
- ),
54
- 'author_url' => array(
55
- 'default' => '',
56
- 'sanitize_callback' => 'esc_url_raw',
57
- ),
58
- 'karma' => array(
59
- 'default' => 0,
60
- 'sanitize_callback' => 'absint',
61
- ),
62
- 'status' => array(
63
- 'sanitize_callback' => 'sanitize_key',
64
- ),
65
- 'date' => array(
66
- 'default' => current_time( 'mysql' ),
67
- ),
68
- 'date_gmt' => array(
69
- 'default' => current_time( 'mysql', true ),
70
- ),
71
- ),
72
  ),
 
 
73
  ) );
74
 
75
  register_rest_route( 'wp/v2', '/comments/(?P<id>[\d]+)', array(
@@ -78,49 +33,14 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
78
  'callback' => array( $this, 'get_item' ),
79
  'permission_callback' => array( $this, 'get_item_permissions_check' ),
80
  'args' => array(
81
- 'context' => array(
82
- 'default' => 'view',
83
- ),
84
  ),
85
  ),
86
  array(
87
  'methods' => WP_REST_Server::EDITABLE,
88
  'callback' => array( $this, 'update_item' ),
89
  'permission_callback' => array( $this, 'update_item_permissions_check' ),
90
- 'args' => array(
91
- 'post' => array(
92
- 'sanitize_callback' => 'absint',
93
- ),
94
- 'type' => array(
95
- 'sanitize_callback' => 'sanitize_key',
96
- ),
97
- 'parent' => array(
98
- 'sanitize_callback' => 'absint',
99
- ),
100
- 'content' => array(
101
- 'sanitize_callback' => 'wp_filter_post_kses',
102
- ),
103
- 'author' => array(
104
- 'sanitize_callback' => 'absint',
105
- ),
106
- 'author_name' => array(
107
- 'sanitize_callback' => 'sanitize_text_field',
108
- ),
109
- 'author_email' => array(
110
- 'sanitize_callback' => 'sanitize_email',
111
- ),
112
- 'author_url' => array(
113
- 'sanitize_callback' => 'esc_url_raw',
114
- ),
115
- 'karma' => array(
116
- 'sanitize_callback' => 'absint',
117
- ),
118
- 'status' => array(
119
- 'sanitize_callback' => 'sanitize_key',
120
- ),
121
- 'date' => array(),
122
- 'date_gmt' => array(),
123
- ),
124
  ),
125
  array(
126
  'methods' => WP_REST_Server::DELETABLE,
@@ -130,11 +50,8 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
130
  'force' => array(),
131
  ),
132
  ),
133
- ) );
134
 
135
- register_rest_route( 'wp/v2', '/comments/schema', array(
136
- 'methods' => WP_REST_Server::READABLE,
137
- 'callback' => array( $this, 'get_public_item_schema' ),
138
  ) );
139
  }
140
 
@@ -201,12 +118,14 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
201
 
202
  $comment = get_comment( $id );
203
  if ( empty( $comment ) ) {
204
- return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) );
205
  }
206
 
207
- $post = get_post( $comment->comment_post_ID );
208
- if ( empty( $post ) ) {
209
- return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
 
 
210
  }
211
 
212
  $data = $this->prepare_item_for_response( $comment, $request );
@@ -226,18 +145,46 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
226
  return new WP_Error( 'rest_comment_exists', __( 'Cannot create existing comment.' ), array( 'status' => 400 ) );
227
  }
228
 
229
- $post = get_post( $request['post'] );
230
- if ( empty( $post ) ) {
231
- return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
232
- }
233
-
234
  $prepared_comment = $this->prepare_item_for_database( $request );
 
235
  // Setting remaining values before wp_insert_comment so we can
236
  // use wp_allow_comment().
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  $prepared_comment['comment_author_IP'] = '127.0.0.1';
238
  $prepared_comment['comment_agent'] = '';
239
  $prepared_comment['comment_approved'] = wp_allow_comment( $prepared_comment );
240
 
 
 
 
 
 
 
 
 
241
  $prepared_comment = apply_filters( 'rest_pre_insert_comment', $prepared_comment, $request );
242
 
243
  $comment_id = wp_insert_comment( $prepared_comment );
@@ -253,10 +200,10 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
253
  $this->update_additional_fields_for_object( get_comment( $comment_id ), $request );
254
 
255
  $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
256
- $response = $this->get_item( array(
257
- 'id' => $comment_id,
258
- 'context' => $context,
259
- ) );
260
  $response = rest_ensure_response( $response );
261
  if ( is_wp_error( $response ) ) {
262
  return $response;
@@ -264,6 +211,15 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
264
  $response->set_status( 201 );
265
  $response->header( 'Location', rest_url( '/wp/v2/comments/' . $comment_id ) );
266
 
 
 
 
 
 
 
 
 
 
267
  return $response;
268
  }
269
 
@@ -278,7 +234,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
278
 
279
  $comment = get_comment( $id );
280
  if ( empty( $comment ) ) {
281
- return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) );
282
  }
283
 
284
  if ( isset( $request['type'] ) && $request['type'] !== $comment->comment_type ) {
@@ -308,24 +264,22 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
308
 
309
  $this->update_additional_fields_for_object( get_comment( $id ), $request );
310
 
311
- $response = $this->get_item( array(
312
- 'id' => $id,
313
- 'context' => 'edit',
314
- ) );
315
- $response = rest_ensure_response( $response );
316
- if ( is_wp_error( $response ) ) {
317
- return $response;
318
- }
319
- $response->header( 'Location', rest_url( '/wp/v2/comments/' . $comment->comment_ID ) );
320
 
321
- return $response;
 
 
 
322
  }
323
 
324
  /**
325
  * Delete a comment.
326
  *
327
  * @param WP_REST_Request $request Full details about the request.
328
- * @return WP_Error|array
329
  */
330
  public function delete_item( $request ) {
331
  $id = (int) $request['id'];
@@ -333,23 +287,27 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
333
 
334
  $comment = get_comment( $id );
335
  if ( empty( $comment ) ) {
336
- return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) );
337
  }
338
 
339
  /**
340
- * Filter whether the comment type supports trashing.
 
 
341
  *
342
- * @param boolean $supports_trash Does the comment type support trashing?
343
- * @param stdClass $comment Comment we're attempting to trash.
344
  */
345
- $supports_trash = apply_filters( 'rest_comment_type_trashable', ( EMPTY_TRASH_DAYS > 0 ), $comment );
346
 
347
- $get_request = new WP_REST_Request( 'GET', rest_url( '/wp/v2/comments/' . $id ) );
 
348
  $get_request->set_param( 'context', 'edit' );
349
  $response = $this->prepare_item_for_response( $comment, $get_request );
350
 
351
  if ( $force ) {
352
  $result = wp_delete_comment( $comment->comment_ID, true );
 
353
  } else {
354
  // If we don't support trashing for this type, error out
355
  if ( ! $supports_trash ) {
@@ -357,12 +315,29 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
357
  }
358
 
359
  $result = wp_trash_comment( $comment->comment_ID );
 
360
  }
361
 
 
 
 
 
 
 
 
362
  if ( ! $result ) {
363
  return new WP_Error( 'rest_cannot_delete', __( 'The comment cannot be deleted.' ), array( 'status' => 500 ) );
364
  }
365
 
 
 
 
 
 
 
 
 
 
366
  return $response;
367
  }
368
 
@@ -380,12 +355,12 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
380
  $post = get_post( (int) $request['post'] );
381
 
382
  if ( $post && ! $this->check_read_post_permission( $post ) ) {
383
- return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you cannot read the post for this comment.' ) );
384
  }
385
  }
386
 
387
- if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'manage_comments' ) ) {
388
- return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view comments with edit context.' ), array( 'status' => 403 ) );
389
  }
390
 
391
  return true;
@@ -407,17 +382,17 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
407
  }
408
 
409
  if ( ! $this->check_read_permission( $comment ) ) {
410
- return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot read this comment.' ), array( 'status' => 403 ) );
411
  }
412
 
413
  $post = get_post( $comment->comment_post_ID );
414
 
415
  if ( $post && ! $this->check_read_post_permission( $post ) ) {
416
- return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you cannot read the post for this comment.' ), array( 'status' => 403 ) );
417
  }
418
 
419
  if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
420
- return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this comment with edit context.' ), array( 'status' => 403 ) );
421
  }
422
 
423
  return true;
@@ -431,28 +406,25 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
431
  */
432
  public function create_item_permissions_check( $request ) {
433
 
 
 
 
 
434
  // Limit who can set comment `author`, `karma` or `status` to anything other than the default.
435
  if ( isset( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( 'moderate_comments' ) ) {
436
- return new WP_Error( 'rest_comment_invalid_author', __( 'Comment author invalid.' ), array( 'status' => 403 ) );
437
  }
438
  if ( isset( $request['karma'] ) && $request['karma'] > 0 && ! current_user_can( 'moderate_comments' ) ) {
439
- return new WP_Error( 'rest_comment_invalid_karma', __( 'Sorry, you cannot set karma for comments.' ), array( 'status' => 403 ) );
440
  }
441
  if ( isset( $request['status'] ) && ! current_user_can( 'moderate_comments' ) ) {
442
- return new WP_Error( 'rest_comment_invalid_status', __( 'Sorry, you cannot set status for comments.' ), array( 'status' => 403 ) );
443
  }
444
 
445
- // If the post id isn't specified, presume we can create.
446
- if ( ! isset( $request['post'] ) ) {
447
- return true;
448
- }
449
-
450
- $post = get_post( (int) $request['post'] );
451
-
452
- if ( $post ) {
453
 
454
  if ( ! $this->check_read_post_permission( $post ) ) {
455
- return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you cannot read the post for this comment.' ), array( 'status' => 403 ) );
456
  }
457
 
458
  if ( ! comments_open( $post->ID ) ) {
@@ -476,7 +448,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
476
  $comment = get_comment( $id );
477
 
478
  if ( $comment && ! $this->check_edit_permission( $comment ) ) {
479
- return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you can not edit this comment.' ), array( 'status' => 403 ) );
480
  }
481
 
482
  return true;
@@ -497,7 +469,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
497
  *
498
  * @param object $comment Comment object.
499
  * @param WP_REST_Request $request Request object.
500
- * @return array $fields
501
  */
502
  public function prepare_item_for_response( $comment, $request ) {
503
  $data = array(
@@ -511,8 +483,8 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
511
  'author_ip' => $comment->comment_author_IP,
512
  'author_avatar_urls' => rest_get_avatar_urls( $comment->comment_author_email ),
513
  'author_user_agent' => $comment->comment_agent,
514
- 'date' => rest_mysql_to_rfc3339( $comment->comment_date ),
515
- 'date_gmt' => rest_mysql_to_rfc3339( $comment->comment_date_gmt ),
516
  'content' => array(
517
  'rendered' => apply_filters( 'comment_text', $comment->comment_content, $comment ),
518
  'raw' => $comment->comment_content,
@@ -528,11 +500,20 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
528
  $data = $this->add_additional_fields_to_object( $data, $request );
529
 
530
  // Wrap the data in a response object
531
- $data = rest_ensure_response( $data );
532
 
533
- $data->add_links( $this->prepare_links( $comment ) );
534
 
535
- return apply_filters( 'rest_prepare_comment', $data, $comment, $request );
 
 
 
 
 
 
 
 
 
536
  }
537
 
538
  /**
@@ -591,17 +572,17 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
591
  * @return array $prepared_args
592
  */
593
  protected function prepare_items_query( $request ) {
594
- $order_by = sanitize_key( $request['orderby'] );
595
 
596
  $prepared_args = array(
597
- 'number' => $request['per_page'],
598
- 'post_id' => $request['post'] ? $request['post'] : '',
599
- 'parent' => isset( $request['parent'] ) ? $request['parent'] : '',
600
- 'search' => $request['search'],
601
- 'orderby' => $this->normalize_query_param( $order_by ),
602
- 'order' => $request['order'],
603
- 'status' => 'approve',
604
- 'type' => 'comment',
 
605
  );
606
 
607
  $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 );
@@ -645,6 +626,9 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
645
  case 'parent':
646
  $normalized = $prefix . 'parent';
647
  break;
 
 
 
648
  default:
649
  $normalized = $prefix . $query_param;
650
  break;
@@ -731,18 +715,13 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
731
  $date_data = rest_get_date_with_gmt( $request['date'] );
732
 
733
  if ( ! empty( $date_data ) ) {
734
- list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) =
735
- $date_data;
736
- } else {
737
- return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ), array( 'status' => 400 ) );
738
  }
739
  } elseif ( ! empty( $request['date_gmt'] ) ) {
740
  $date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
741
 
742
  if ( ! empty( $date_data ) ) {
743
  list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
744
- } else {
745
- return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ), array( 'status' => 400 ) );
746
  }
747
  }
748
 
@@ -760,8 +739,9 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
760
  $avatar_sizes = rest_get_avatar_sizes();
761
  foreach ( $avatar_sizes as $size ) {
762
  $avatar_properties[ $size ] = array(
763
- 'description' => 'Avatar URL with image size of ' . $size . ' pixels.',
764
- 'type' => 'uri',
 
765
  'context' => array( 'embed', 'view', 'edit' ),
766
  );
767
  }
@@ -772,113 +752,133 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
772
  'type' => 'object',
773
  'properties' => array(
774
  'id' => array(
775
- 'description' => 'Unique identifier for the object.',
776
  'type' => 'integer',
777
  'context' => array( 'view', 'edit', 'embed' ),
778
  'readonly' => true,
779
- ),
780
  'author' => array(
781
- 'description' => 'The ID of the user object, if author was a user.',
782
  'type' => 'integer',
783
  'context' => array( 'view', 'edit', 'embed' ),
784
- ),
785
  'author_avatar_urls' => array(
786
- 'description' => 'Avatar URLs for the object author.',
787
  'type' => 'object',
788
  'context' => array( 'view', 'edit', 'embed' ),
789
  'readonly' => true,
790
  'properties' => $avatar_properties,
791
- ),
792
  'author_email' => array(
793
- 'description' => 'Email address for the object author.',
794
  'type' => 'string',
795
  'format' => 'email',
796
  'context' => array( 'edit' ),
797
- ),
798
  'author_ip' => array(
799
- 'description' => 'IP address for the object author.',
800
  'type' => 'string',
801
  'context' => array( 'edit' ),
802
  'readonly' => true,
803
- ),
804
  'author_name' => array(
805
- 'description' => 'Display name for the object author.',
806
  'type' => 'string',
807
  'context' => array( 'view', 'edit', 'embed' ),
 
 
 
808
  ),
 
809
  'author_url' => array(
810
- 'description' => 'URL for the object author.',
811
  'type' => 'string',
812
  'format' => 'uri',
813
  'context' => array( 'view', 'edit', 'embed' ),
814
- ),
815
  'author_user_agent' => array(
816
- 'description' => 'User agent for the object author.',
817
  'type' => 'string',
818
  'context' => array( 'edit' ),
819
  'readonly' => true,
820
- ),
821
  'content' => array(
822
- 'description' => 'The content for the object.',
823
  'type' => 'object',
824
  'context' => array( 'view', 'edit', 'embed' ),
825
  'properties' => array(
826
  'raw' => array(
827
- 'description' => 'Content for the object, as it exists in the database.',
828
  'type' => 'string',
829
  'context' => array( 'edit' ),
830
- ),
831
  'rendered' => array(
832
- 'description' => 'Content for the object, transformed for display.',
833
  'type' => 'string',
834
  'context' => array( 'view', 'edit', 'embed' ),
835
- ),
836
  ),
837
  ),
 
 
 
 
 
838
  'date' => array(
839
- 'description' => 'The date the object was published.',
840
  'type' => 'string',
841
  'format' => 'date-time',
842
  'context' => array( 'view', 'edit', 'embed' ),
843
  ),
844
  'date_gmt' => array(
845
- 'description' => 'The date the object was published as GMT.',
846
  'type' => 'string',
847
  'format' => 'date-time',
848
- 'context' => array( 'edit' ),
849
  ),
850
  'karma' => array(
851
- 'description' => 'Karma for the object.',
852
  'type' => 'integer',
853
  'context' => array( 'edit' ),
854
- 'readonly' => true,
855
  ),
856
  'link' => array(
857
- 'description' => 'URL to the object.',
858
  'type' => 'string',
859
  'format' => 'uri',
860
  'context' => array( 'view', 'edit', 'embed' ),
861
  'readonly' => true,
862
  ),
863
  'parent' => array(
864
- 'description' => 'The ID for the parent of the object.',
865
  'type' => 'integer',
866
  'context' => array( 'view', 'edit', 'embed' ),
 
 
 
867
  ),
868
  'post' => array(
869
- 'description' => 'The ID of the associated post object.',
870
  'type' => 'integer',
871
  'context' => array( 'view', 'edit' ),
 
 
 
872
  ),
873
  'status' => array(
874
- 'description' => 'State of the object.',
875
  'type' => 'string',
876
  'context' => array( 'view', 'edit' ),
 
 
 
877
  ),
878
  'type' => array(
879
- 'description' => 'Type of Comment for the object.',
880
  'type' => 'string',
881
  'context' => array( 'view', 'edit', 'embed' ),
 
 
 
 
882
  ),
883
  ),
884
  );
@@ -892,76 +892,101 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
892
  */
893
  public function get_collection_params() {
894
  $query_params = parent::get_collection_params();
 
 
 
895
  $query_params['author_email'] = array(
896
  'default' => null,
897
- 'description' => 'Limit result set to that from a specific author email.',
898
  'format' => 'email',
899
  'sanitize_callback' => 'sanitize_email',
900
  'type' => 'string',
901
  );
 
 
 
 
 
 
902
  $query_params['karma'] = array(
903
  'default' => null,
904
- 'description' => 'Limit result set to that of a particular comment karma.',
905
  'sanitize_callback' => 'absint',
906
  'type' => 'integer',
907
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908
  $query_params['parent'] = array(
909
  'default' => null,
910
- 'description' => 'Limit result set to that of a specific comment parent id.',
911
  'sanitize_callback' => 'absint',
912
  'type' => 'integer',
913
  );
914
  $query_params['post'] = array(
915
  'default' => null,
916
- 'description' => 'Limit result set to comments assigned to a specific post id.',
917
  'sanitize_callback' => 'absint',
918
  'type' => 'integer',
919
  );
920
  $query_params['post_author'] = array(
921
  'default' => null,
922
- 'description' => 'Limit result set to comments associated with posts of a specific post author id.',
923
  'sanitize_callback' => 'absint',
924
  'type' => 'integer',
925
  );
926
  $query_params['post_slug'] = array(
927
  'default' => null,
928
- 'description' => 'Limit result set to comments associated with posts of a specific post slug.',
929
  'sanitize_callback' => 'sanitize_title',
930
  'type' => 'string',
931
  );
932
  $query_params['post_parent'] = array(
933
  'default' => null,
934
- 'description' => 'Limit result set to comments associated with posts of a specific post parent id.',
935
  'sanitize_callback' => 'absint',
936
  'type' => 'integer',
937
  );
938
  $query_params['post_status'] = array(
939
  'default' => null,
940
- 'description' => 'Limit result set to comments associated with posts of a specific post status.',
941
  'sanitize_callback' => 'sanitize_key',
942
  'type' => 'string',
943
  );
944
  $query_params['post_type'] = array(
945
  'default' => null,
946
- 'description' => 'Limit result set to comments associated with posts of a specific post type.',
947
  'sanitize_callback' => 'sanitize_key',
948
  'type' => 'string',
949
  );
950
  $query_params['status'] = array(
951
  'default' => 'approve',
952
- 'description' => 'Limit result set to comments assigned a specific status.',
953
  'sanitize_callback' => 'sanitize_key',
954
  'type' => 'string',
955
  );
956
  $query_params['type'] = array(
957
  'default' => 'comment',
958
- 'description' => 'Limit result set to comments assigned a specific type.',
959
  'sanitize_callback' => 'sanitize_key',
960
  'type' => 'string',
961
  );
962
  $query_params['user'] = array(
963
  'default' => null,
964
- 'description' => 'Limit result set to comments assigned to a specific user id.',
965
  'sanitize_callback' => 'absint',
966
  'type' => 'integer',
967
  );
10
  */
11
  public function register_routes() {
12
 
 
13
  register_rest_route( 'wp/v2', '/comments', array(
14
  array(
15
  'methods' => WP_REST_Server::READABLE,
16
  'callback' => array( $this, 'get_items' ),
17
  'permission_callback' => array( $this, 'get_items_permissions_check' ),
18
+ 'args' => $this->get_collection_params(),
19
  ),
20
  array(
21
  'methods' => WP_REST_Server::CREATABLE,
22
  'callback' => array( $this, 'create_item' ),
23
  'permission_callback' => array( $this, 'create_item_permissions_check' ),
24
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  ),
26
+
27
+ 'schema' => array( $this, 'get_public_item_schema' ),
28
  ) );
29
 
30
  register_rest_route( 'wp/v2', '/comments/(?P<id>[\d]+)', array(
33
  'callback' => array( $this, 'get_item' ),
34
  'permission_callback' => array( $this, 'get_item_permissions_check' ),
35
  'args' => array(
36
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
 
 
37
  ),
38
  ),
39
  array(
40
  'methods' => WP_REST_Server::EDITABLE,
41
  'callback' => array( $this, 'update_item' ),
42
  'permission_callback' => array( $this, 'update_item_permissions_check' ),
43
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  ),
45
  array(
46
  'methods' => WP_REST_Server::DELETABLE,
50
  'force' => array(),
51
  ),
52
  ),
 
53
 
54
+ 'schema' => array( $this, 'get_public_item_schema' ),
 
 
55
  ) );
56
  }
57
 
118
 
119
  $comment = get_comment( $id );
120
  if ( empty( $comment ) ) {
121
+ return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment id.' ), array( 'status' => 404 ) );
122
  }
123
 
124
+ if ( ! empty( $comment->comment_post_ID ) ) {
125
+ $post = get_post( $comment->comment_post_ID );
126
+ if ( empty( $post ) ) {
127
+ return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
128
+ }
129
  }
130
 
131
  $data = $this->prepare_item_for_response( $comment, $request );
145
  return new WP_Error( 'rest_comment_exists', __( 'Cannot create existing comment.' ), array( 'status' => 400 ) );
146
  }
147
 
 
 
 
 
 
148
  $prepared_comment = $this->prepare_item_for_database( $request );
149
+
150
  // Setting remaining values before wp_insert_comment so we can
151
  // use wp_allow_comment().
152
+ if ( ! isset( $prepared_comment['comment_date_gmt'] ) ) {
153
+ $prepared_comment['comment_date_gmt'] = current_time( 'mysql', true );
154
+ }
155
+
156
+ // Set author data if the user's logged in
157
+ $missing_author = empty( $prepared_comment['user_id'] )
158
+ && empty( $prepared_comment['comment_author'] )
159
+ && empty( $prepared_comment['comment_author_email'] )
160
+ && empty( $prepared_comment['comment_author_url'] );
161
+
162
+ if ( is_user_logged_in() && $missing_author ) {
163
+ $user = wp_get_current_user();
164
+ $prepared_comment['user_id'] = $user->ID;
165
+ $prepared_comment['comment_author'] = $user->display_name;
166
+ $prepared_comment['comment_author_email'] = $user->user_email;
167
+ $prepared_comment['comment_author_url'] = $user->user_url;
168
+ }
169
+
170
+ if ( ! isset( $prepared_comment['comment_author_email'] ) ) {
171
+ $prepared_comment['comment_author_email'] = '';
172
+ }
173
+ if ( ! isset( $prepared_comment['comment_author_url'] ) ) {
174
+ $prepared_comment['comment_author_url'] = '';
175
+ }
176
  $prepared_comment['comment_author_IP'] = '127.0.0.1';
177
  $prepared_comment['comment_agent'] = '';
178
  $prepared_comment['comment_approved'] = wp_allow_comment( $prepared_comment );
179
 
180
+ /**
181
+ * Filter a comment before it is inserted via the REST API.
182
+ *
183
+ * Allows modification of the comment right before it is inserted via `wp_insert_comment`.
184
+ *
185
+ * @param array $prepared_comment The prepared comment data for `wp_insert_comment`.
186
+ * @param WP_REST_Request $request Request used to insert the comment.
187
+ */
188
  $prepared_comment = apply_filters( 'rest_pre_insert_comment', $prepared_comment, $request );
189
 
190
  $comment_id = wp_insert_comment( $prepared_comment );
200
  $this->update_additional_fields_for_object( get_comment( $comment_id ), $request );
201
 
202
  $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
203
+ $get_request = new WP_REST_Request;
204
+ $get_request->set_param( 'id', $comment_id );
205
+ $get_request->set_param( 'context', $context );
206
+ $response = $this->get_item( $get_request );
207
  $response = rest_ensure_response( $response );
208
  if ( is_wp_error( $response ) ) {
209
  return $response;
211
  $response->set_status( 201 );
212
  $response->header( 'Location', rest_url( '/wp/v2/comments/' . $comment_id ) );
213
 
214
+ /**
215
+ * Fires after a comment is created or updated via the REST API.
216
+ *
217
+ * @param array $prepared_comment Inserted comment data.
218
+ * @param WP_REST_Request $request The request sent to the API.
219
+ * @param bool $creating True when creating a comment, false when updating.
220
+ */
221
+ do_action( 'rest_insert_comment', $prepared_comment, $request, true );
222
+
223
  return $response;
224
  }
225
 
234
 
235
  $comment = get_comment( $id );
236
  if ( empty( $comment ) ) {
237
+ return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment id.' ), array( 'status' => 404 ) );
238
  }
239
 
240
  if ( isset( $request['type'] ) && $request['type'] !== $comment->comment_type ) {
264
 
265
  $this->update_additional_fields_for_object( get_comment( $id ), $request );
266
 
267
+ $get_request = new WP_REST_Request;
268
+ $get_request->set_param( 'id', $id );
269
+ $get_request->set_param( 'context', 'edit' );
270
+ $response = $this->get_item( $get_request );
 
 
 
 
 
271
 
272
+ /* This action is documented in lib/endpoints/class-wp-rest-comments-controller.php */
273
+ do_action( 'rest_insert_comment', $prepared_args, $request, false );
274
+
275
+ return rest_ensure_response( $response );
276
  }
277
 
278
  /**
279
  * Delete a comment.
280
  *
281
  * @param WP_REST_Request $request Full details about the request.
282
+ * @return WP_Error|WP_REST_Response
283
  */
284
  public function delete_item( $request ) {
285
  $id = (int) $request['id'];
287
 
288
  $comment = get_comment( $id );
289
  if ( empty( $comment ) ) {
290
+ return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment id.' ), array( 'status' => 404 ) );
291
  }
292
 
293
  /**
294
+ * Filter whether a comment is trashable.
295
+ *
296
+ * Return false to disable trash support for the post.
297
  *
298
+ * @param boolean $supports_trash Whether the post type support trashing.
299
+ * @param WP_Post $comment The comment object being considered for trashing support.
300
  */
301
+ $supports_trash = apply_filters( 'rest_comment_trashable', ( EMPTY_TRASH_DAYS > 0 ), $comment );
302
 
303
+ $get_request = new WP_REST_Request;
304
+ $get_request->set_param( 'id', $id );
305
  $get_request->set_param( 'context', 'edit' );
306
  $response = $this->prepare_item_for_response( $comment, $get_request );
307
 
308
  if ( $force ) {
309
  $result = wp_delete_comment( $comment->comment_ID, true );
310
+ $status = 'deleted';
311
  } else {
312
  // If we don't support trashing for this type, error out
313
  if ( ! $supports_trash ) {
315
  }
316
 
317
  $result = wp_trash_comment( $comment->comment_ID );
318
+ $status = 'trashed';
319
  }
320
 
321
+ $data = $response->get_data();
322
+ $data = array(
323
+ 'data' => $data,
324
+ $status => true,
325
+ );
326
+ $response->set_data( $data );
327
+
328
  if ( ! $result ) {
329
  return new WP_Error( 'rest_cannot_delete', __( 'The comment cannot be deleted.' ), array( 'status' => 500 ) );
330
  }
331
 
332
+ /**
333
+ * Fires after a comment is deleted via the REST API.
334
+ *
335
+ * @param object $comment The deleted comment data.
336
+ * @param array $data Delete status data.
337
+ * @param WP_REST_Request $request The request sent to the API.
338
+ */
339
+ do_action( 'rest_delete_comment', $comment, $data, $request );
340
+
341
  return $response;
342
  }
343
 
355
  $post = get_post( (int) $request['post'] );
356
 
357
  if ( $post && ! $this->check_read_post_permission( $post ) ) {
358
+ return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you cannot read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) );
359
  }
360
  }
361
 
362
+ if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
363
+ return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view comments with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
364
  }
365
 
366
  return true;
382
  }
383
 
384
  if ( ! $this->check_read_permission( $comment ) ) {
385
+ return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot read this comment.' ), array( 'status' => rest_authorization_required_code() ) );
386
  }
387
 
388
  $post = get_post( $comment->comment_post_ID );
389
 
390
  if ( $post && ! $this->check_read_post_permission( $post ) ) {
391
+ return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you cannot read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) );
392
  }
393
 
394
  if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
395
+ return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this comment with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
396
  }
397
 
398
  return true;
406
  */
407
  public function create_item_permissions_check( $request ) {
408
 
409
+ if ( ! is_user_logged_in() && get_option( 'comment_registration' ) ) {
410
+ return new WP_Error( 'rest_comment_login_required', __( 'Sorry, you must be logged in to comment.' ), array( 'status' => 401 ) );
411
+ }
412
+
413
  // Limit who can set comment `author`, `karma` or `status` to anything other than the default.
414
  if ( isset( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( 'moderate_comments' ) ) {
415
+ return new WP_Error( 'rest_comment_invalid_author', __( 'Comment author invalid.' ), array( 'status' => rest_authorization_required_code() ) );
416
  }
417
  if ( isset( $request['karma'] ) && $request['karma'] > 0 && ! current_user_can( 'moderate_comments' ) ) {
418
+ return new WP_Error( 'rest_comment_invalid_karma', __( 'Sorry, you cannot set karma for comments.' ), array( 'status' => rest_authorization_required_code() ) );
419
  }
420
  if ( isset( $request['status'] ) && ! current_user_can( 'moderate_comments' ) ) {
421
+ return new WP_Error( 'rest_comment_invalid_status', __( 'Sorry, you cannot set status for comments.' ), array( 'status' => rest_authorization_required_code() ) );
422
  }
423
 
424
+ if ( ! empty( $request['post'] ) && $post = get_post( (int) $request['post'] ) ) {
 
 
 
 
 
 
 
425
 
426
  if ( ! $this->check_read_post_permission( $post ) ) {
427
+ return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you cannot read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) );
428
  }
429
 
430
  if ( ! comments_open( $post->ID ) ) {
448
  $comment = get_comment( $id );
449
 
450
  if ( $comment && ! $this->check_edit_permission( $comment ) ) {
451
+ return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you can not edit this comment.' ), array( 'status' => rest_authorization_required_code() ) );
452
  }
453
 
454
  return true;
469
  *
470
  * @param object $comment Comment object.
471
  * @param WP_REST_Request $request Request object.
472
+ * @return WP_REST_Response
473
  */
474
  public function prepare_item_for_response( $comment, $request ) {
475
  $data = array(
483
  'author_ip' => $comment->comment_author_IP,
484
  'author_avatar_urls' => rest_get_avatar_urls( $comment->comment_author_email ),
485
  'author_user_agent' => $comment->comment_agent,
486
+ 'date' => mysql_to_rfc3339( $comment->comment_date ),
487
+ 'date_gmt' => mysql_to_rfc3339( $comment->comment_date_gmt ),
488
  'content' => array(
489
  'rendered' => apply_filters( 'comment_text', $comment->comment_content, $comment ),
490
  'raw' => $comment->comment_content,
500
  $data = $this->add_additional_fields_to_object( $data, $request );
501
 
502
  // Wrap the data in a response object
503
+ $response = rest_ensure_response( $data );
504
 
505
+ $response->add_links( $this->prepare_links( $comment ) );
506
 
507
+ /**
508
+ * Filter a comment returned from the API.
509
+ *
510
+ * Allows modification of the comment right before it is returned.
511
+ *
512
+ * @param WP_REST_Response $response The response object.
513
+ * @param object $comment The original comment object.
514
+ * @param WP_REST_Request $request Request used to generate the response.
515
+ */
516
+ return apply_filters( 'rest_prepare_comment', $response, $comment, $request );
517
  }
518
 
519
  /**
572
  * @return array $prepared_args
573
  */
574
  protected function prepare_items_query( $request ) {
 
575
 
576
  $prepared_args = array(
577
+ 'comment__in' => $request['include'],
578
+ 'number' => $request['per_page'],
579
+ 'post_id' => $request['post'] ? $request['post'] : '',
580
+ 'parent' => isset( $request['parent'] ) ? $request['parent'] : '',
581
+ 'search' => $request['search'],
582
+ 'orderby' => $this->normalize_query_param( $request['orderby'] ),
583
+ 'order' => $request['order'],
584
+ 'status' => 'approve',
585
+ 'type' => 'comment',
586
  );
587
 
588
  $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 );
626
  case 'parent':
627
  $normalized = $prefix . 'parent';
628
  break;
629
+ case 'include':
630
+ $normalized = 'comment__in';
631
+ break;
632
  default:
633
  $normalized = $prefix . $query_param;
634
  break;
715
  $date_data = rest_get_date_with_gmt( $request['date'] );
716
 
717
  if ( ! empty( $date_data ) ) {
718
+ list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
 
 
 
719
  }
720
  } elseif ( ! empty( $request['date_gmt'] ) ) {
721
  $date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
722
 
723
  if ( ! empty( $date_data ) ) {
724
  list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
 
 
725
  }
726
  }
727
 
739
  $avatar_sizes = rest_get_avatar_sizes();
740
  foreach ( $avatar_sizes as $size ) {
741
  $avatar_properties[ $size ] = array(
742
+ 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
743
+ 'type' => 'string',
744
+ 'format' => 'uri',
745
  'context' => array( 'embed', 'view', 'edit' ),
746
  );
747
  }
752
  'type' => 'object',
753
  'properties' => array(
754
  'id' => array(
755
+ 'description' => __( 'Unique identifier for the object.' ),
756
  'type' => 'integer',
757
  'context' => array( 'view', 'edit', 'embed' ),
758
  'readonly' => true,
759
+ ),
760
  'author' => array(
761
+ 'description' => __( 'The id of the user object, if author was a user.' ),
762
  'type' => 'integer',
763
  'context' => array( 'view', 'edit', 'embed' ),
764
+ ),
765
  'author_avatar_urls' => array(
766
+ 'description' => __( 'Avatar URLs for the object author.' ),
767
  'type' => 'object',
768
  'context' => array( 'view', 'edit', 'embed' ),
769
  'readonly' => true,
770
  'properties' => $avatar_properties,
771
+ ),
772
  'author_email' => array(
773
+ 'description' => __( 'Email address for the object author.' ),
774
  'type' => 'string',
775
  'format' => 'email',
776
  'context' => array( 'edit' ),
777
+ ),
778
  'author_ip' => array(
779
+ 'description' => __( 'IP address for the object author.' ),
780
  'type' => 'string',
781
  'context' => array( 'edit' ),
782
  'readonly' => true,
783
+ ),
784
  'author_name' => array(
785
+ 'description' => __( 'Display name for the object author.' ),
786
  'type' => 'string',
787
  'context' => array( 'view', 'edit', 'embed' ),
788
+ 'arg_options' => array(
789
+ 'sanitize_callback' => 'sanitize_text_field',
790
+ 'default' => '',
791
  ),
792
+ ),
793
  'author_url' => array(
794
+ 'description' => __( 'URL for the object author.' ),
795
  'type' => 'string',
796
  'format' => 'uri',
797
  'context' => array( 'view', 'edit', 'embed' ),
798
+ ),
799
  'author_user_agent' => array(
800
+ 'description' => __( 'User agent for the object author.' ),
801
  'type' => 'string',
802
  'context' => array( 'edit' ),
803
  'readonly' => true,
804
+ ),
805
  'content' => array(
806
+ 'description' => __( 'The content for the object.' ),
807
  'type' => 'object',
808
  'context' => array( 'view', 'edit', 'embed' ),
809
  'properties' => array(
810
  'raw' => array(
811
+ 'description' => __( 'Content for the object, as it exists in the database.' ),
812
  'type' => 'string',
813
  'context' => array( 'edit' ),
814
+ ),
815
  'rendered' => array(
816
+ 'description' => __( 'Content for the object, transformed for display.' ),
817
  'type' => 'string',
818
  'context' => array( 'view', 'edit', 'embed' ),
 
819
  ),
820
  ),
821
+ 'arg_options' => array(
822
+ 'sanitize_callback' => 'wp_filter_post_kses',
823
+ 'default' => '',
824
+ ),
825
+ ),
826
  'date' => array(
827
+ 'description' => __( 'The date the object was published.' ),
828
  'type' => 'string',
829
  'format' => 'date-time',
830
  'context' => array( 'view', 'edit', 'embed' ),
831
  ),
832
  'date_gmt' => array(
833
+ 'description' => __( 'The date the object was published as GMT.' ),
834
  'type' => 'string',
835
  'format' => 'date-time',
836
+ 'context' => array( 'view', 'edit' ),
837
  ),
838
  'karma' => array(
839
+ 'description' => __( 'Karma for the object.' ),
840
  'type' => 'integer',
841
  'context' => array( 'edit' ),
 
842
  ),
843
  'link' => array(
844
+ 'description' => __( 'URL to the object.' ),
845
  'type' => 'string',
846
  'format' => 'uri',
847
  'context' => array( 'view', 'edit', 'embed' ),
848
  'readonly' => true,
849
  ),
850
  'parent' => array(
851
+ 'description' => __( 'The id for the parent of the object.' ),
852
  'type' => 'integer',
853
  'context' => array( 'view', 'edit', 'embed' ),
854
+ 'arg_options' => array(
855
+ 'default' => 0,
856
+ ),
857
  ),
858
  'post' => array(
859
+ 'description' => __( 'The id of the associated post object.' ),
860
  'type' => 'integer',
861
  'context' => array( 'view', 'edit' ),
862
+ 'arg_options' => array(
863
+ 'default' => 0,
864
+ ),
865
  ),
866
  'status' => array(
867
+ 'description' => __( 'State of the object.' ),
868
  'type' => 'string',
869
  'context' => array( 'view', 'edit' ),
870
+ 'arg_options' => array(
871
+ 'sanitize_callback' => 'sanitize_key',
872
+ ),
873
  ),
874
  'type' => array(
875
+ 'description' => __( 'Type of Comment for the object.' ),
876
  'type' => 'string',
877
  'context' => array( 'view', 'edit', 'embed' ),
878
+ 'arg_options' => array(
879
+ 'sanitize_callback' => 'sanitize_key',
880
+ 'default' => '',
881
+ ),
882
  ),
883
  ),
884
  );
892
  */
893
  public function get_collection_params() {
894
  $query_params = parent::get_collection_params();
895
+
896
+ $query_params['context']['default'] = 'view';
897
+
898
  $query_params['author_email'] = array(
899
  'default' => null,
900
+ 'description' => __( 'Limit result set to that from a specific author email.' ),
901
  'format' => 'email',
902
  'sanitize_callback' => 'sanitize_email',
903
  'type' => 'string',
904
  );
905
+ $query_params['include'] = array(
906
+ 'description' => __( 'Limit result set to specific ids.' ),
907
+ 'type' => 'array',
908
+ 'default' => array(),
909
+ 'sanitize_callback' => 'wp_parse_id_list',
910
+ );
911
  $query_params['karma'] = array(
912
  'default' => null,
913
+ 'description' => __( 'Limit result set to that of a particular comment karma.' ),
914
  'sanitize_callback' => 'absint',
915
  'type' => 'integer',
916
  );
917
+ $query_params['order'] = array(
918
+ 'description' => __( 'Order sort attribute ascending or descending.' ),
919
+ 'type' => 'string',
920
+ 'sanitize_callback' => 'sanitize_key',
921
+ 'default' => 'asc',
922
+ 'enum' => array(
923
+ 'asc',
924
+ 'desc',
925
+ ),
926
+ );
927
+ $query_params['orderby'] = array(
928
+ 'description' => __( 'Sort collection by object attribute.' ),
929
+ 'type' => 'string',
930
+ 'sanitize_callback' => 'sanitize_key',
931
+ 'default' => 'date_gmt',
932
+ );
933
  $query_params['parent'] = array(
934
  'default' => null,
935
+ 'description' => __( 'Limit result set to that of a specific comment parent id.' ),
936
  'sanitize_callback' => 'absint',
937
  'type' => 'integer',
938
  );
939
  $query_params['post'] = array(
940
  'default' => null,
941
+ 'description' => __( 'Limit result set to comments assigned to a specific post id.' ),
942
  'sanitize_callback' => 'absint',
943
  'type' => 'integer',
944
  );
945
  $query_params['post_author'] = array(
946
  'default' => null,
947
+ 'description' => __( 'Limit result set to comments associated with posts of a specific post author id.' ),
948
  'sanitize_callback' => 'absint',
949
  'type' => 'integer',
950
  );
951
  $query_params['post_slug'] = array(
952
  'default' => null,
953
+ 'description' => __( 'Limit result set to comments associated with posts of a specific post slug.' ),
954
  'sanitize_callback' => 'sanitize_title',
955
  'type' => 'string',
956
  );
957
  $query_params['post_parent'] = array(
958
  'default' => null,
959
+ 'description' => __( 'Limit result set to comments associated with posts of a specific post parent id.' ),
960
  'sanitize_callback' => 'absint',
961
  'type' => 'integer',
962
  );
963
  $query_params['post_status'] = array(
964
  'default' => null,
965
+ 'description' => __( 'Limit result set to comments associated with posts of a specific post status.' ),
966
  'sanitize_callback' => 'sanitize_key',
967
  'type' => 'string',
968
  );
969
  $query_params['post_type'] = array(
970
  'default' => null,
971
+ 'description' => __( 'Limit result set to comments associated with posts of a specific post type.' ),
972
  'sanitize_callback' => 'sanitize_key',
973
  'type' => 'string',
974
  );
975
  $query_params['status'] = array(
976
  'default' => 'approve',
977
+ 'description' => __( 'Limit result set to comments assigned a specific status.' ),
978
  'sanitize_callback' => 'sanitize_key',
979
  'type' => 'string',
980
  );
981
  $query_params['type'] = array(
982
  'default' => 'comment',
983
+ 'description' => __( 'Limit result set to comments assigned a specific type.' ),
984
  'sanitize_callback' => 'sanitize_key',
985
  'type' => 'string',
986
  );
987
  $query_params['user'] = array(
988
  'default' => null,
989
+ 'description' => __( 'Limit result set to comments assigned to a specific user id.' ),
990
  'sanitize_callback' => 'absint',
991
  'type' => 'integer',
992
  );
lib/endpoints/class-wp-rest-controller.php CHANGED
@@ -11,124 +11,124 @@ abstract class WP_REST_Controller {
11
  }
12
 
13
  /**
14
- * Get a collection of items
15
  *
16
  * @param WP_REST_Request $request Full data about the request.
17
  * @return WP_Error|WP_REST_Response
18
  */
19
  public function get_items( $request ) {
20
- return new WP_Error( 'invalid-method', __( 'Method not implemented. Must be over-ridden in subclass.' ), array( 'status' => 405 ) );
21
  }
22
 
23
  /**
24
- * Get one item from the collection
25
  *
26
  * @param WP_REST_Request $request Full data about the request.
27
  * @return WP_Error|WP_REST_Response
28
  */
29
  public function get_item( $request ) {
30
- return new WP_Error( 'invalid-method', __( 'Method not implemented. Must be over-ridden in subclass.' ), array( 'status' => 405 ) );
31
  }
32
 
33
  /**
34
- * Create one item from the collection
35
  *
36
  * @param WP_REST_Request $request Full data about the request.
37
- * @return WP_Error|WP_REST_Request
38
  */
39
  public function create_item( $request ) {
40
- return new WP_Error( 'invalid-method', __( 'Method not implemented. Must be over-ridden in subclass.' ), array( 'status' => 405 ) );
41
  }
42
 
43
  /**
44
- * Update one item from the collection
45
  *
46
  * @param WP_REST_Request $request Full data about the request.
47
- * @return WP_Error|WP_REST_Request
48
  */
49
  public function update_item( $request ) {
50
- return new WP_Error( 'invalid-method', __( 'Method not implemented. Must be over-ridden in subclass.' ), array( 'status' => 405 ) );
51
  }
52
 
53
  /**
54
- * Delete one item from the collection
55
  *
56
  * @param WP_REST_Request $request Full data about the request.
57
- * @return WP_Error|WP_REST_Request
58
  */
59
  public function delete_item( $request ) {
60
- return new WP_Error( 'invalid-method', __( 'Method not implemented. Must be over-ridden in subclass.' ), array( 'status' => 405 ) );
61
  }
62
 
63
  /**
64
- * Check if a given request has access to get items
65
  *
66
  * @param WP_REST_Request $request Full data about the request.
67
  * @return WP_Error|bool
68
  */
69
  public function get_items_permissions_check( $request ) {
70
- return new WP_Error( 'invalid-method', __( 'Method not implemented. Must be over-ridden in subclass.' ), array( 'status' => 405 ) );
71
  }
72
 
73
  /**
74
- * Check if a given request has access to get a specific item
75
  *
76
  * @param WP_REST_Request $request Full data about the request.
77
  * @return WP_Error|bool
78
  */
79
  public function get_item_permissions_check( $request ) {
80
- return new WP_Error( 'invalid-method', __( 'Method not implemented. Must be over-ridden in subclass.' ), array( 'status' => 405 ) );
81
  }
82
 
83
  /**
84
- * Check if a given request has access to create items
85
  *
86
  * @param WP_REST_Request $request Full data about the request.
87
  * @return WP_Error|bool
88
  */
89
  public function create_item_permissions_check( $request ) {
90
- return new WP_Error( 'invalid-method', __( 'Method not implemented. Must be over-ridden in subclass.' ), array( 'status' => 405 ) );
91
  }
92
 
93
  /**
94
- * Check if a given request has access to update a specific item
95
  *
96
  * @param WP_REST_Request $request Full data about the request.
97
  * @return WP_Error|bool
98
  */
99
  public function update_item_permissions_check( $request ) {
100
- return new WP_Error( 'invalid-method', __( 'Method not implemented. Must be over-ridden in subclass.' ), array( 'status' => 405 ) );
101
  }
102
 
103
  /**
104
- * Check if a given request has access to delete a specific item
105
  *
106
  * @param WP_REST_Request $request Full data about the request.
107
  * @return WP_Error|bool
108
  */
109
  public function delete_item_permissions_check( $request ) {
110
- return new WP_Error( 'invalid-method', __( 'Method not implemented. Must be over-ridden in subclass.' ), array( 'status' => 405 ) );
111
  }
112
 
113
  /**
114
- * Prepare the item for create or update operation
115
  *
116
- * @param WP_REST_Request $request Request object
117
  * @return WP_Error|object $prepared_item
118
  */
119
  protected function prepare_item_for_database( $request ) {
120
- return new WP_Error( 'invalid-method', __( 'Method not implemented. Must be over-ridden in subclass.' ), array( 'status' => 405 ) );
121
  }
122
 
123
  /**
124
- * Prepare the item for the REST response
125
  *
126
  * @param mixed $item WordPress representation of the item.
127
  * @param WP_REST_Request $request Request object.
128
  * @return mixed
129
  */
130
  public function prepare_item_for_response( $item, $request ) {
131
- return new WP_Error( 'invalid-method', __( 'Method not implemented. Must be over-ridden in subclass.' ), array( 'status' => 405 ) );
132
  }
133
 
134
  /**
@@ -152,7 +152,7 @@ abstract class WP_REST_Controller {
152
  }
153
 
154
  /**
155
- * Filter a response based on the context defined in the schema
156
  *
157
  * @param array $data
158
  * @param string $context
@@ -186,7 +186,7 @@ abstract class WP_REST_Controller {
186
  }
187
 
188
  /**
189
- * Get the item's schema, conforming to JSON Schema
190
  *
191
  * @return array
192
  */
@@ -213,26 +213,27 @@ abstract class WP_REST_Controller {
213
  }
214
 
215
  /**
216
- * Get the query params for collections
217
  *
218
  * @return array
219
  */
220
  public function get_collection_params() {
221
  return array(
 
222
  'page' => array(
223
- 'description' => 'Current page of the collection.',
224
  'type' => 'integer',
225
  'default' => 1,
226
  'sanitize_callback' => 'absint',
227
  ),
228
  'per_page' => array(
229
- 'description' => 'Maximum number of items to be returned in result set.',
230
  'type' => 'integer',
231
  'default' => 10,
232
  'sanitize_callback' => 'absint',
233
  ),
234
  'search' => array(
235
- 'description' => 'Limit results to those matching a string.',
236
  'type' => 'string',
237
  'sanitize_callback' => 'sanitize_text_field',
238
  ),
@@ -240,11 +241,42 @@ abstract class WP_REST_Controller {
240
  }
241
 
242
  /**
243
- * Add the values from additional fields to a data object
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  *
245
  * @param array $object
246
  * @param WP_REST_Request $request
247
- * @return array modified object with additional fields
248
  */
249
  protected function add_additional_fields_to_object( $object, $request ) {
250
 
@@ -256,7 +288,7 @@ abstract class WP_REST_Controller {
256
  continue;
257
  }
258
 
259
- $object[ $field_name ] = call_user_func( $field_options['get_callback'], $object, $field_name, $request );
260
  }
261
 
262
  return $object;
@@ -278,21 +310,21 @@ abstract class WP_REST_Controller {
278
  continue;
279
  }
280
 
281
- // Don't run the update callbacks if the data wasn't passed in the request
282
  if ( ! isset( $request[ $field_name ] ) ) {
283
  continue;
284
  }
285
 
286
- $result = call_user_func( $field_options['update_callback'], $request[ $field_name ], $object, $field_name, $request );
287
  }
288
  }
289
 
290
  /**
291
- * Add the schema from additional fields to an schema array
292
  *
293
  * The type of object is inferred from the passed schema.
294
  *
295
- * @param array $schema Schema array
296
  */
297
  protected function add_additional_fields_schema( $schema ) {
298
  if ( ! $schema || ! isset( $schema['title'] ) ) {
@@ -300,7 +332,7 @@ abstract class WP_REST_Controller {
300
  }
301
 
302
  /**
303
- * Can't use $this->get_object_type otherwise we cause an inf loop
304
  */
305
  $object_type = $schema['title'];
306
 
@@ -318,7 +350,7 @@ abstract class WP_REST_Controller {
318
  }
319
 
320
  /**
321
- * Get all the registered additional fields for a given object-type
322
  *
323
  * @param string $object_type
324
  * @return array
@@ -360,12 +392,14 @@ abstract class WP_REST_Controller {
360
  /**
361
  * Get an array of endpoint arguments from the item schema for the controller.
362
  *
363
- * @param $add_required_flag Whether to use the 'required' flag from the schema proprties.
364
- * This is because update requests will not have any required params
365
- * Where as create requests will.
366
- * @return array
 
 
367
  */
368
- public function get_endpoint_args_for_item_schema( $add_required_flag = true ) {
369
 
370
  $schema = $this->get_item_schema();
371
  $schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
@@ -373,7 +407,7 @@ abstract class WP_REST_Controller {
373
 
374
  foreach ( $schema_properties as $field_id => $params ) {
375
 
376
- // Anything marked as readonly should not be a arg
377
  if ( ! empty( $params['readonly'] ) ) {
378
  continue;
379
  }
@@ -383,16 +417,22 @@ abstract class WP_REST_Controller {
383
  'sanitize_callback' => array( $this, 'sanitize_schema_property' ),
384
  );
385
 
386
- if ( isset( $params['default'] ) ) {
387
  $endpoint_args[ $field_id ]['default'] = $params['default'];
388
  }
389
 
390
- if ( $add_required_flag && ! empty( $params['required'] ) ) {
391
  $endpoint_args[ $field_id ]['required'] = true;
392
  }
393
 
394
- // Merge in any options provided by the schema property
395
  if ( isset( $params['arg_options'] ) ) {
 
 
 
 
 
 
396
  $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
397
  }
398
  }
@@ -401,7 +441,7 @@ abstract class WP_REST_Controller {
401
  }
402
 
403
  /**
404
- * Validate an parameter value that's based on a property from the item schema.
405
  *
406
  * @param mixed $value
407
  * @param WP_REST_Request $request
@@ -412,7 +452,7 @@ abstract class WP_REST_Controller {
412
 
413
  /**
414
  * We don't currently validate against empty values, as lots of checks
415
- * can unintentially fail, as the callback will often handle an empty
416
  * value it's self.
417
  */
418
  if ( ! $value ) {
@@ -437,7 +477,7 @@ abstract class WP_REST_Controller {
437
  return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not of type %s' ), $parameter, 'integer' ) );
438
  }
439
 
440
- if ( 'string' === $property['type']&& ! is_string( $value ) ) {
441
  return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not of type %s' ), $parameter, 'string' ) );
442
  }
443
 
@@ -461,7 +501,7 @@ abstract class WP_REST_Controller {
461
  }
462
 
463
  /**
464
- * Sanitize an parameter value that's based on a property from the item schema.
465
  *
466
  * @param mixed $value
467
  * @param WP_REST_Request $request
@@ -479,7 +519,7 @@ abstract class WP_REST_Controller {
479
  $property = $schema['properties'][ $parameter ];
480
 
481
  if ( 'integer' === $property['type'] ) {
482
- return intval( $value );
483
  }
484
 
485
  if ( isset( $property['format'] ) ) {
@@ -489,7 +529,7 @@ abstract class WP_REST_Controller {
489
 
490
  case 'email' :
491
  // as sanitize_email is very lossy, we just want to
492
- // make sure the string is safe
493
  if ( sanitize_email( $value ) ) {
494
  return sanitize_email( $value );
495
  }
11
  }
12
 
13
  /**
14
+ * Get a collection of items.
15
  *
16
  * @param WP_REST_Request $request Full data about the request.
17
  * @return WP_Error|WP_REST_Response
18
  */
19
  public function get_items( $request ) {
20
+ return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
21
  }
22
 
23
  /**
24
+ * Get one item from the collection.
25
  *
26
  * @param WP_REST_Request $request Full data about the request.
27
  * @return WP_Error|WP_REST_Response
28
  */
29
  public function get_item( $request ) {
30
+ return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
31
  }
32
 
33
  /**
34
+ * Create one item from the collection.
35
  *
36
  * @param WP_REST_Request $request Full data about the request.
37
+ * @return WP_Error|WP_REST_Response
38
  */
39
  public function create_item( $request ) {
40
+ return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
41
  }
42
 
43
  /**
44
+ * Update one item from the collection.
45
  *
46
  * @param WP_REST_Request $request Full data about the request.
47
+ * @return WP_Error|WP_REST_Response
48
  */
49
  public function update_item( $request ) {
50
+ return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
51
  }
52
 
53
  /**
54
+ * Delete one item from the collection.
55
  *
56
  * @param WP_REST_Request $request Full data about the request.
57
+ * @return WP_Error|WP_REST_Response
58
  */
59
  public function delete_item( $request ) {
60
+ return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
61
  }
62
 
63
  /**
64
+ * Check if a given request has access to get items.
65
  *
66
  * @param WP_REST_Request $request Full data about the request.
67
  * @return WP_Error|bool
68
  */
69
  public function get_items_permissions_check( $request ) {
70
+ return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
71
  }
72
 
73
  /**
74
+ * Check if a given request has access to get a specific item.
75
  *
76
  * @param WP_REST_Request $request Full data about the request.
77
  * @return WP_Error|bool
78
  */
79
  public function get_item_permissions_check( $request ) {
80
+ return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
81
  }
82
 
83
  /**
84
+ * Check if a given request has access to create items.
85
  *
86
  * @param WP_REST_Request $request Full data about the request.
87
  * @return WP_Error|bool
88
  */
89
  public function create_item_permissions_check( $request ) {
90
+ return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
91
  }
92
 
93
  /**
94
+ * Check if a given request has access to update a specific item.
95
  *
96
  * @param WP_REST_Request $request Full data about the request.
97
  * @return WP_Error|bool
98
  */
99
  public function update_item_permissions_check( $request ) {
100
+ return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
101
  }
102
 
103
  /**
104
+ * Check if a given request has access to delete a specific item.
105
  *
106
  * @param WP_REST_Request $request Full data about the request.
107
  * @return WP_Error|bool
108
  */
109
  public function delete_item_permissions_check( $request ) {
110
+ return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
111
  }
112
 
113
  /**
114
+ * Prepare the item for create or update operation.
115
  *
116
+ * @param WP_REST_Request $request Request object.
117
  * @return WP_Error|object $prepared_item
118
  */
119
  protected function prepare_item_for_database( $request ) {
120
+ return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
121
  }
122
 
123
  /**
124
+ * Prepare the item for the REST response.
125
  *
126
  * @param mixed $item WordPress representation of the item.
127
  * @param WP_REST_Request $request Request object.
128
  * @return mixed
129
  */
130
  public function prepare_item_for_response( $item, $request ) {
131
+ return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
132
  }
133
 
134
  /**
152
  }
153
 
154
  /**
155
+ * Filter a response based on the context defined in the schema.
156
  *
157
  * @param array $data
158
  * @param string $context
186
  }
187
 
188
  /**
189
+ * Get the item's schema, conforming to JSON Schema.
190
  *
191
  * @return array
192
  */
213
  }
214
 
215
  /**
216
+ * Get the query params for collections.
217
  *
218
  * @return array
219
  */
220
  public function get_collection_params() {
221
  return array(
222
+ 'context' => $this->get_context_param(),
223
  'page' => array(
224
+ 'description' => __( 'Current page of the collection.' ),
225
  'type' => 'integer',
226
  'default' => 1,
227
  'sanitize_callback' => 'absint',
228
  ),
229
  'per_page' => array(
230
+ 'description' => __( 'Maximum number of items to be returned in result set.' ),
231
  'type' => 'integer',
232
  'default' => 10,
233
  'sanitize_callback' => 'absint',
234
  ),
235
  'search' => array(
236
+ 'description' => __( 'Limit results to those matching a string.' ),
237
  'type' => 'string',
238
  'sanitize_callback' => 'sanitize_text_field',
239
  ),
241
  }
242
 
243
  /**
244
+ * Get the magical context param.
245
+ *
246
+ * Ensures consistent description between endpoints, and populates enum from schema.
247
+ *
248
+ * @param array $args
249
+ * @return array
250
+ */
251
+ public function get_context_param( $args = array() ) {
252
+ $param_details = array(
253
+ 'description' => __( 'Scope under which the request is made; determines fields present in response.' ),
254
+ 'type' => 'string',
255
+ );
256
+ $schema = $this->get_item_schema();
257
+ $contexts = array();
258
+ if ( empty( $schema['properties'] ) ) {
259
+ return array_merge( $param_details, $args );
260
+ }
261
+ $contexts = array();
262
+ foreach ( $schema['properties'] as $key => $attributes ) {
263
+ if ( ! empty( $attributes['context'] ) ) {
264
+ $contexts = array_merge( $contexts, $attributes['context'] );
265
+ }
266
+ }
267
+ if ( ! empty( $contexts ) ) {
268
+ $param_details['enum'] = array_unique( $contexts );
269
+ rsort( $param_details['enum'] );
270
+ }
271
+ return array_merge( $param_details, $args );
272
+ }
273
+
274
+ /**
275
+ * Add the values from additional fields to a data object.
276
  *
277
  * @param array $object
278
  * @param WP_REST_Request $request
279
+ * @return array modified object with additional fields.
280
  */
281
  protected function add_additional_fields_to_object( $object, $request ) {
282
 
288
  continue;
289
  }
290
 
291
+ $object[ $field_name ] = call_user_func( $field_options['get_callback'], $object, $field_name, $request, $this->get_object_type() );
292
  }
293
 
294
  return $object;
310
  continue;
311
  }
312
 
313
+ // Don't run the update callbacks if the data wasn't passed in the request.
314
  if ( ! isset( $request[ $field_name ] ) ) {
315
  continue;
316
  }
317
 
318
+ $result = call_user_func( $field_options['update_callback'], $request[ $field_name ], $object, $field_name, $request, $this->get_object_type() );
319
  }
320
  }
321
 
322
  /**
323
+ * Add the schema from additional fields to an schema array.
324
  *
325
  * The type of object is inferred from the passed schema.
326
  *
327
+ * @param array $schema Schema array.
328
  */
329
  protected function add_additional_fields_schema( $schema ) {
330
  if ( ! $schema || ! isset( $schema['title'] ) ) {
332
  }
333
 
334
  /**
335
+ * Can't use $this->get_object_type otherwise we cause an inf loop.
336
  */
337
  $object_type = $schema['title'];
338
 
350
  }
351
 
352
  /**
353
+ * Get all the registered additional fields for a given object-type.
354
  *
355
  * @param string $object_type
356
  * @return array
392
  /**
393
  * Get an array of endpoint arguments from the item schema for the controller.
394
  *
395
+ * @param string $method HTTP method of the request. The arguments
396
+ * for `CREATABLE` requests are checked for required
397
+ * values and may fall-back to a given default, this
398
+ * is not done on `EDITABLE` requests. Default is
399
+ * WP_REST_Server::CREATABLE.
400
+ * @return array $endpoint_args
401
  */
402
+ public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
403
 
404
  $schema = $this->get_item_schema();
405
  $schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
407
 
408
  foreach ( $schema_properties as $field_id => $params ) {
409
 
410
+ // Arguments specified as `readonly` are not allowed to be set.
411
  if ( ! empty( $params['readonly'] ) ) {
412
  continue;
413
  }
417
  'sanitize_callback' => array( $this, 'sanitize_schema_property' ),
418
  );
419
 
420
+ if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) {
421
  $endpoint_args[ $field_id ]['default'] = $params['default'];
422
  }
423
 
424
+ if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) {
425
  $endpoint_args[ $field_id ]['required'] = true;
426
  }
427
 
428
+ // Merge in any options provided by the schema property.
429
  if ( isset( $params['arg_options'] ) ) {
430
+
431
+ // Only use required / default from arg_options on CREATABLE endpoints.
432
+ if ( WP_REST_Server::CREATABLE !== $method ) {
433
+ $params['arg_options'] = array_diff_key( $params['arg_options'], array( 'required' => '', 'default' => '' ) );
434
+ }
435
+
436
  $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
437
  }
438
  }
441
  }
442
 
443
  /**
444
+ * Validate a parameter value that's based on a property from the item schema.
445
  *
446
  * @param mixed $value
447
  * @param WP_REST_Request $request
452
 
453
  /**
454
  * We don't currently validate against empty values, as lots of checks
455
+ * can unintentionally fail, as the callback will often handle an empty
456
  * value it's self.
457
  */
458
  if ( ! $value ) {
477
  return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not of type %s' ), $parameter, 'integer' ) );
478
  }
479
 
480
+ if ( 'string' === $property['type'] && ! is_string( $value ) ) {
481
  return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not of type %s' ), $parameter, 'string' ) );
482
  }
483
 
501
  }
502
 
503
  /**
504
+ * Sanitize a parameter value that's based on a property from the item schema.
505
  *
506
  * @param mixed $value
507
  * @param WP_REST_Request $request
519
  $property = $schema['properties'][ $parameter ];
520
 
521
  if ( 'integer' === $property['type'] ) {
522
+ return (int) $value;
523
  }
524
 
525
  if ( isset( $property['format'] ) ) {
529
 
530
  case 'email' :
531
  // as sanitize_email is very lossy, we just want to
532
+ // make sure the string is safe.
533
  if ( sanitize_email( $value ) ) {
534
  return sanitize_email( $value );
535
  }
lib/endpoints/class-wp-rest-meta-controller.php CHANGED
@@ -40,23 +40,16 @@ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
40
  'methods' => WP_REST_Server::READABLE,
41
  'callback' => array( $this, 'get_items' ),
42
  'permission_callback' => array( $this, 'get_items_permissions_check' ),
43
- 'args' => array(
44
- 'context' => array(
45
- 'default' => 'view',
46
- ),
47
- ),
48
  ),
49
  array(
50
  'methods' => WP_REST_Server::CREATABLE,
51
  'callback' => array( $this, 'create_item' ),
52
  'permission_callback' => array( $this, 'create_item_permissions_check' ),
53
- 'args' => array(
54
- 'key' => array(
55
- 'required' => true,
56
- ),
57
- 'value' => array(),
58
- ),
59
  ),
 
 
60
  ) );
61
  register_rest_route( 'wp/v2', '/' . $this->parent_base . '/(?P<parent_id>[\d]+)/meta/(?P<id>[\d]+)', array(
62
  array(
@@ -64,30 +57,27 @@ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
64
  'callback' => array( $this, 'get_item' ),
65
  'permission_callback' => array( $this, 'get_item_permissions_check' ),
66
  'args' => array(
67
- 'context' => array(
68
- 'default' => 'view',
69
- ),
70
  ),
71
  ),
72
  array(
73
  'methods' => WP_REST_Server::EDITABLE,
74
  'callback' => array( $this, 'update_item' ),
75
  'permission_callback' => array( $this, 'update_item_permissions_check' ),
76
- 'args' => array(
77
- 'key' => array(),
78
- 'value' => array(),
79
- ),
80
  ),
81
  array(
82
  'methods' => WP_REST_Server::DELETABLE,
83
  'callback' => array( $this, 'delete_item' ),
84
  'permission_callback' => array( $this, 'delete_item_permissions_check' ),
85
- 'args' => array(),
 
 
 
 
86
  ),
87
- ) );
88
- register_rest_route( 'wp/v2', $this->parent_base . '/meta/schema', array(
89
- 'methods' => WP_REST_Server::READABLE,
90
- 'callback' => array( $this, 'get_public_item_schema' ),
91
  ) );
92
  }
93
 
@@ -106,17 +96,22 @@ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
106
  */
107
  'properties' => array(
108
  'id' => array(
109
- 'description' => 'Unique identifier for the object.',
110
- 'type' => 'int',
111
  'context' => array( 'edit' ),
 
112
  ),
113
  'key' => array(
114
- 'description' => 'The key for the custom field.',
115
  'type' => 'string',
116
  'context' => array( 'edit' ),
 
 
 
 
117
  ),
118
  'value' => array(
119
- 'description' => 'The value of the custom field.',
120
  'type' => 'string',
121
  'context' => array( 'edit' ),
122
  ),
@@ -125,6 +120,19 @@ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
125
  return $schema;
126
  }
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  /**
129
  * Get the meta ID column for the relevant table.
130
  *
@@ -190,7 +198,7 @@ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
190
  $meta = get_metadata_by_mid( $this->parent_type, $mid );
191
 
192
  if ( empty( $meta ) ) {
193
- return new WP_Error( 'rest_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) );
194
  }
195
 
196
  if ( absint( $meta->$parent_column ) !== $parent_id ) {
@@ -239,6 +247,14 @@ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
239
  $parent_column = $this->get_parent_column();
240
  $response->add_link( 'about', rest_url( 'wp/' . $this->parent_base . '/' . $data->$parent_column ), array( 'embeddable' => true ) );
241
 
 
 
 
 
 
 
 
 
242
  return apply_filters( 'rest_prepare_meta_value', $response, $request );
243
  }
244
 
@@ -256,7 +272,7 @@ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
256
  $current = get_metadata_by_mid( $this->parent_type, $mid );
257
 
258
  if ( empty( $current ) ) {
259
- return new WP_Error( 'rest_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) );
260
  }
261
 
262
  if ( absint( $current->$parent_column ) !== $parent_id ) {
@@ -319,6 +335,15 @@ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
319
  ) );
320
  $response = $this->get_item( $request );
321
 
 
 
 
 
 
 
 
 
 
322
  return rest_ensure_response( $response );
323
  }
324
 
@@ -355,14 +380,14 @@ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
355
  return new WP_Error( $code, __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) );
356
  }
357
 
358
- if ( is_protected_meta( $request['key'] ) ) {
359
- return new WP_Error( 'rest_meta_protected', sprintf( __( '%s is marked as a protected field.' ), $request['key'] ), array( 'status' => 403 ) );
360
- }
361
-
362
  if ( empty( $request['key'] ) ) {
363
  return new WP_Error( 'rest_meta_invalid_key', __( 'Invalid meta key.' ), array( 'status' => 400 ) );
364
  }
365
 
 
 
 
 
366
  $meta_key = wp_slash( $request['key'] );
367
  $value = wp_slash( $request['value'] );
368
 
@@ -381,7 +406,10 @@ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
381
 
382
  $response->set_status( 201 );
383
  $data = $response->get_data();
384
- $response->header( 'Location', rest_url( $this->parent_base . '/' . $parent_id . '/meta/' . $data['id'] ) );
 
 
 
385
 
386
  return $response;
387
  }
@@ -406,7 +434,7 @@ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
406
  $current = get_metadata_by_mid( $this->parent_type, $mid );
407
 
408
  if ( empty( $current ) ) {
409
- return new WP_Error( 'rest_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) );
410
  }
411
 
412
  if ( absint( $current->$parent_column ) !== (int) $parent_id ) {
@@ -427,6 +455,13 @@ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
427
  return new WP_Error( 'rest_meta_could_not_delete', __( 'Could not delete meta.' ), array( 'status' => 500 ) );
428
  }
429
 
 
 
 
 
 
 
 
430
  return rest_ensure_response( array( 'message' => __( 'Deleted meta' ) ) );
431
  }
432
  }
40
  'methods' => WP_REST_Server::READABLE,
41
  'callback' => array( $this, 'get_items' ),
42
  'permission_callback' => array( $this, 'get_items_permissions_check' ),
43
+ 'args' => $this->get_collection_params(),
 
 
 
 
44
  ),
45
  array(
46
  'methods' => WP_REST_Server::CREATABLE,
47
  'callback' => array( $this, 'create_item' ),
48
  'permission_callback' => array( $this, 'create_item_permissions_check' ),
49
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
 
 
 
 
 
50
  ),
51
+
52
+ 'schema' => array( $this, 'get_public_item_schema' ),
53
  ) );
54
  register_rest_route( 'wp/v2', '/' . $this->parent_base . '/(?P<parent_id>[\d]+)/meta/(?P<id>[\d]+)', array(
55
  array(
57
  'callback' => array( $this, 'get_item' ),
58
  'permission_callback' => array( $this, 'get_item_permissions_check' ),
59
  'args' => array(
60
+ 'context' => $this->get_context_param( array( 'default' => 'edit' ) ),
 
 
61
  ),
62
  ),
63
  array(
64
  'methods' => WP_REST_Server::EDITABLE,
65
  'callback' => array( $this, 'update_item' ),
66
  'permission_callback' => array( $this, 'update_item_permissions_check' ),
67
+ 'args' => $this->get_endpoint_args_for_item_schema( false ),
 
 
 
68
  ),
69
  array(
70
  'methods' => WP_REST_Server::DELETABLE,
71
  'callback' => array( $this, 'delete_item' ),
72
  'permission_callback' => array( $this, 'delete_item_permissions_check' ),
73
+ 'args' => array(
74
+ 'force' => array(
75
+ 'default' => false,
76
+ ),
77
+ ),
78
  ),
79
+
80
+ 'schema' => array( $this, 'get_public_item_schema' ),
 
 
81
  ) );
82
  }
83
 
96
  */
97
  'properties' => array(
98
  'id' => array(
99
+ 'description' => __( 'Unique identifier for the object.' ),
100
+ 'type' => 'integer',
101
  'context' => array( 'edit' ),
102
+ 'readonly' => true,
103
  ),
104
  'key' => array(
105
+ 'description' => __( 'The key for the custom field.' ),
106
  'type' => 'string',
107
  'context' => array( 'edit' ),
108
+ 'required' => true,
109
+ 'arg_options' => array(
110
+ 'sanitize_callback' => 'sanitize_text_field',
111
+ ),
112
  ),
113
  'value' => array(
114
+ 'description' => __( 'The value of the custom field.' ),
115
  'type' => 'string',
116
  'context' => array( 'edit' ),
117
  ),
120
  return $schema;
121
  }
122
 
123
+ /**
124
+ * Get the query params for collections
125
+ *
126
+ * @return array
127
+ */
128
+ public function get_collection_params() {
129
+ $params = parent::get_collection_params();
130
+ $new_params = array();
131
+ $new_params['context'] = $params['context'];
132
+ $new_params['context']['default'] = 'edit';
133
+ return $new_params;
134
+ }
135
+
136
  /**
137
  * Get the meta ID column for the relevant table.
138
  *
198
  $meta = get_metadata_by_mid( $this->parent_type, $mid );
199
 
200
  if ( empty( $meta ) ) {
201
+ return new WP_Error( 'rest_meta_invalid_id', __( 'Invalid meta id.' ), array( 'status' => 404 ) );
202
  }
203
 
204
  if ( absint( $meta->$parent_column ) !== $parent_id ) {
247
  $parent_column = $this->get_parent_column();
248
  $response->add_link( 'about', rest_url( 'wp/' . $this->parent_base . '/' . $data->$parent_column ), array( 'embeddable' => true ) );
249
 
250
+ /**
251
+ * Filter a meta value returned from the API.
252
+ *
253
+ * Allows modification of the meta value right before it is returned.
254
+ *
255
+ * @param array $response Key value array of meta data: id, key, value.
256
+ * @param WP_REST_Request $request Request used to generate the response.
257
+ */
258
  return apply_filters( 'rest_prepare_meta_value', $response, $request );
259
  }
260
 
272
  $current = get_metadata_by_mid( $this->parent_type, $mid );
273
 
274
  if ( empty( $current ) ) {
275
+ return new WP_Error( 'rest_meta_invalid_id', __( 'Invalid meta id.' ), array( 'status' => 404 ) );
276
  }
277
 
278
  if ( absint( $current->$parent_column ) !== $parent_id ) {
335
  ) );
336
  $response = $this->get_item( $request );
337
 
338
+ /**
339
+ * Fires after meta is added to an object or updated via the REST API.
340
+ *
341
+ * @param array $value The inserted meta data.
342
+ * @param WP_REST_Request $request The request sent to the API.
343
+ * @param bool $creating True when adding meta, false when updating.
344
+ */
345
+ do_action( 'rest_insert_meta', $value, $request, false );
346
+
347
  return rest_ensure_response( $response );
348
  }
349
 
380
  return new WP_Error( $code, __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) );
381
  }
382
 
 
 
 
 
383
  if ( empty( $request['key'] ) ) {
384
  return new WP_Error( 'rest_meta_invalid_key', __( 'Invalid meta key.' ), array( 'status' => 400 ) );
385
  }
386
 
387
+ if ( is_protected_meta( $request['key'] ) ) {
388
+ return new WP_Error( 'rest_meta_protected', sprintf( __( '%s is marked as a protected field.' ), $request['key'] ), array( 'status' => 403 ) );
389
+ }
390
+
391
  $meta_key = wp_slash( $request['key'] );
392
  $value = wp_slash( $request['value'] );
393
 
406
 
407
  $response->set_status( 201 );
408
  $data = $response->get_data();
409
+ $response->header( 'Location', rest_url( 'wp/v2' . '/' . $this->parent_base . '/' . $parent_id . '/meta/' . $data['id'] ) );
410
+
411
+ /* This action is documented in lib/endpoints/class-wp-rest-meta-controller.php */
412
+ do_action( 'rest_insert_meta', $data, $request, true );
413
 
414
  return $response;
415
  }
434
  $current = get_metadata_by_mid( $this->parent_type, $mid );
435
 
436
  if ( empty( $current ) ) {
437
+ return new WP_Error( 'rest_meta_invalid_id', __( 'Invalid meta id.' ), array( 'status' => 404 ) );
438
  }
439
 
440
  if ( absint( $current->$parent_column ) !== (int) $parent_id ) {
455
  return new WP_Error( 'rest_meta_could_not_delete', __( 'Could not delete meta.' ), array( 'status' => 500 ) );
456
  }
457
 
458
+ /**
459
+ * Fires after a meta value is deleted via the REST API.
460
+ *
461
+ * @param WP_REST_Request $request The request sent to the API.
462
+ */
463
+ do_action( 'rest_delete_meta', $request );
464
+
465
  return rest_ensure_response( array( 'message' => __( 'Deleted meta' ) ) );
466
  }
467
  }
lib/endpoints/class-wp-rest-meta-posts-controller.php CHANGED
@@ -45,16 +45,16 @@ class WP_REST_Meta_Posts_Controller extends WP_REST_Meta_Controller {
45
  $parent = get_post( (int) $request['parent_id'] );
46
 
47
  if ( empty( $parent ) || empty( $parent->ID ) ) {
48
- return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
49
  }
50
 
51
  if ( ! $this->parent_controller->check_read_permission( $parent ) ) {
52
- return new WP_Error( 'rest_forbidden', __( 'Sorry, you cannot view this post.' ), array( 'status' => 403 ) );
53
  }
54
 
55
  $post_type = get_post_type_object( $parent->post_type );
56
  if ( ! current_user_can( $post_type->cap->edit_post, $parent->ID ) ) {
57
- return new WP_Error( 'rest_forbidden', __( 'Sorry, you cannot view the meta for this post.' ), array( 'status' => 403 ) );
58
  }
59
  return true;
60
  }
@@ -99,16 +99,16 @@ class WP_REST_Meta_Posts_Controller extends WP_REST_Meta_Controller {
99
  $parent = get_post( (int) $request['parent_id'] );
100
 
101
  if ( empty( $parent ) || empty( $parent->ID ) ) {
102
- return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
103
  }
104
 
105
  if ( ! $this->parent_controller->check_read_permission( $parent ) ) {
106
- return new WP_Error( 'rest_forbidden', __( 'Sorry, you cannot view this post.' ), array( 'status' => 403 ) );
107
  }
108
 
109
  $post_type = get_post_type_object( $parent->post_type );
110
  if ( ! current_user_can( $post_type->cap->delete_post, $parent->ID ) ) {
111
- return new WP_Error( 'rest_forbidden', __( 'Sorry, you cannot delete the meta for this post.' ), array( 'status' => 403 ) );
112
  }
113
  return true;
114
  }
45
  $parent = get_post( (int) $request['parent_id'] );
46
 
47
  if ( empty( $parent ) || empty( $parent->ID ) ) {
48
+ return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
49
  }
50
 
51
  if ( ! $this->parent_controller->check_read_permission( $parent ) ) {
52
+ return new WP_Error( 'rest_forbidden', __( 'Sorry, you cannot view this post.' ), array( 'status' => rest_authorization_required_code() ) );
53
  }
54
 
55
  $post_type = get_post_type_object( $parent->post_type );
56
  if ( ! current_user_can( $post_type->cap->edit_post, $parent->ID ) ) {
57
+ return new WP_Error( 'rest_forbidden', __( 'Sorry, you cannot view the meta for this post.' ), array( 'status' => rest_authorization_required_code() ) );
58
  }
59
  return true;
60
  }
99
  $parent = get_post( (int) $request['parent_id'] );
100
 
101
  if ( empty( $parent ) || empty( $parent->ID ) ) {
102
+ return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
103
  }
104
 
105
  if ( ! $this->parent_controller->check_read_permission( $parent ) ) {
106
+ return new WP_Error( 'rest_forbidden', __( 'Sorry, you cannot view this post.' ), array( 'status' => rest_authorization_required_code() ) );
107
  }
108
 
109
  $post_type = get_post_type_object( $parent->post_type );
110
  if ( ! current_user_can( $post_type->cap->delete_post, $parent->ID ) ) {
111
+ return new WP_Error( 'rest_forbidden', __( 'Sorry, you cannot delete the meta for this post.' ), array( 'status' => rest_authorization_required_code() ) );
112
  }
113
  return true;
114
  }
lib/endpoints/class-wp-rest-post-statuses-controller.php CHANGED
@@ -8,18 +8,23 @@ class WP_REST_Post_Statuses_Controller extends WP_REST_Controller {
8
  public function register_routes() {
9
 
10
  register_rest_route( 'wp/v2', '/statuses', array(
11
- 'methods' => WP_REST_Server::READABLE,
12
- 'callback' => array( $this, 'get_items' ),
13
- ) );
14
-
15
- register_rest_route( 'wp/v2', '/statuses/schema', array(
16
- 'methods' => WP_REST_Server::READABLE,
17
- 'callback' => array( $this, 'get_public_item_schema' ),
18
  ) );
19
 
20
  register_rest_route( 'wp/v2', '/statuses/(?P<status>[\w-]+)', array(
21
- 'methods' => WP_REST_Server::READABLE,
22
- 'callback' => array( $this, 'get_item' ),
 
 
 
 
 
 
23
  ) );
24
  }
25
 
@@ -69,7 +74,7 @@ class WP_REST_Post_Statuses_Controller extends WP_REST_Controller {
69
  */
70
  public function prepare_item_for_response( $status, $request ) {
71
  if ( ( false === $status->public && ! is_user_logged_in() ) || ( true === $status->internal && is_user_logged_in() ) ) {
72
- return new WP_Error( 'rest_cannot_read_status', __( 'Cannot view status.' ), array( 'status' => 403 ) );
73
  }
74
 
75
  $data = array(
@@ -86,17 +91,26 @@ class WP_REST_Post_Statuses_Controller extends WP_REST_Controller {
86
  $data = $this->filter_response_by_context( $data, $context );
87
  $data = $this->add_additional_fields_to_object( $data, $request );
88
 
89
- $data = rest_ensure_response( $data );
90
 
91
  $posts_controller = new WP_REST_Posts_Controller( 'post' );
92
 
93
  if ( 'publish' === $status->name ) {
94
- $data->add_link( 'archives', rest_url( '/wp/v2/' . $posts_controller->get_post_type_base( 'post' ) ) );
95
  } else {
96
- $data->add_link( 'archives', add_query_arg( 'status', $status->name, rest_url( '/wp/v2/' . $posts_controller->get_post_type_base( 'post' ) ) ) );
97
  }
98
 
99
- return $data;
 
 
 
 
 
 
 
 
 
100
  }
101
 
102
  /**
@@ -111,37 +125,37 @@ class WP_REST_Post_Statuses_Controller extends WP_REST_Controller {
111
  'type' => 'object',
112
  'properties' => array(
113
  'name' => array(
114
- 'description' => 'The title for the status.',
115
  'type' => 'string',
116
  'context' => array( 'view' ),
117
  ),
118
  'private' => array(
119
- 'description' => 'Whether posts with this status should be private.',
120
  'type' => 'boolean',
121
  'context' => array( 'view' ),
122
  ),
123
  'protected' => array(
124
- 'description' => 'Whether posts with this status should be protected.',
125
  'type' => 'boolean',
126
  'context' => array( 'view' ),
127
  ),
128
  'public' => array(
129
- 'description' => 'Whether posts of this status should be shown in the front end of the site.',
130
  'type' => 'boolean',
131
  'context' => array( 'view' ),
132
  ),
133
  'queryable' => array(
134
- 'description' => 'Whether posts with this status should be publicly-queryable.',
135
  'type' => 'boolean',
136
  'context' => array( 'view' ),
137
  ),
138
  'show_in_list' => array(
139
- 'description' => 'Whether to include posts in the edit listing for their post type.',
140
  'type' => 'boolean',
141
  'context' => array( 'view' ),
142
  ),
143
  'slug' => array(
144
- 'description' => 'An alphanumeric identifier for the status.',
145
  'type' => 'string',
146
  'context' => array( 'view' ),
147
  ),
@@ -150,4 +164,15 @@ class WP_REST_Post_Statuses_Controller extends WP_REST_Controller {
150
  return $this->add_additional_fields_schema( $schema );
151
  }
152
 
 
 
 
 
 
 
 
 
 
 
 
153
  }
8
  public function register_routes() {
9
 
10
  register_rest_route( 'wp/v2', '/statuses', array(
11
+ array(
12
+ 'methods' => WP_REST_Server::READABLE,
13
+ 'callback' => array( $this, 'get_items' ),
14
+ 'args' => $this->get_collection_params(),
15
+ ),
16
+ 'schema' => array( $this, 'get_public_item_schema' ),
 
17
  ) );
18
 
19
  register_rest_route( 'wp/v2', '/statuses/(?P<status>[\w-]+)', array(
20
+ array(
21
+ 'methods' => WP_REST_Server::READABLE,
22
+ 'callback' => array( $this, 'get_item' ),
23
+ 'args' => array(
24
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
25
+ ),
26
+ ),
27
+ 'schema' => array( $this, 'get_public_item_schema' ),
28
  ) );
29
  }
30
 
74
  */
75
  public function prepare_item_for_response( $status, $request ) {
76
  if ( ( false === $status->public && ! is_user_logged_in() ) || ( true === $status->internal && is_user_logged_in() ) ) {
77
+ return new WP_Error( 'rest_cannot_read_status', __( 'Cannot view status.' ), array( 'status' => rest_authorization_required_code() ) );
78
  }
79
 
80
  $data = array(
91
  $data = $this->filter_response_by_context( $data, $context );
92
  $data = $this->add_additional_fields_to_object( $data, $request );
93
 
94
+ $response = rest_ensure_response( $data );
95
 
96
  $posts_controller = new WP_REST_Posts_Controller( 'post' );
97
 
98
  if ( 'publish' === $status->name ) {
99
+ $response->add_link( 'archives', rest_url( '/wp/v2/' . $posts_controller->get_post_type_base( 'post' ) ) );
100
  } else {
101
+ $response->add_link( 'archives', add_query_arg( 'status', $status->name, rest_url( '/wp/v2/' . $posts_controller->get_post_type_base( 'post' ) ) ) );
102
  }
103
 
104
+ /**
105
+ * Filter a status returned from the API.
106
+ *
107
+ * Allows modification of the status data right before it is returned.
108
+ *
109
+ * @param WP_REST_Response $response The response object.
110
+ * @param object $status The original status object.
111
+ * @param WP_REST_Request $request Request used to generate the response.
112
+ */
113
+ return apply_filters( 'rest_prepare_status', $response, $status, $request );
114
  }
115
 
116
  /**
125
  'type' => 'object',
126
  'properties' => array(
127
  'name' => array(
128
+ 'description' => __( 'The title for the status.' ),
129
  'type' => 'string',
130
  'context' => array( 'view' ),
131
  ),
132
  'private' => array(
133
+ 'description' => __( 'Whether posts with this status should be private.' ),
134
  'type' => 'boolean',
135
  'context' => array( 'view' ),
136
  ),
137
  'protected' => array(
138
+ 'description' => __( 'Whether posts with this status should be protected.' ),
139
  'type' => 'boolean',
140
  'context' => array( 'view' ),
141
  ),
142
  'public' => array(
143
+ 'description' => __( 'Whether posts of this status should be shown in the front end of the site.' ),
144
  'type' => 'boolean',
145
  'context' => array( 'view' ),
146
  ),
147
  'queryable' => array(
148
+ 'description' => __( 'Whether posts with this status should be publicly-queryable.' ),
149
  'type' => 'boolean',
150
  'context' => array( 'view' ),
151
  ),
152
  'show_in_list' => array(
153
+ 'description' => __( 'Whether to include posts in the edit listing for their post type.' ),
154
  'type' => 'boolean',
155
  'context' => array( 'view' ),
156
  ),
157
  'slug' => array(
158
+ 'description' => __( 'An alphanumeric identifier for the status.' ),
159
  'type' => 'string',
160
  'context' => array( 'view' ),
161
  ),
164
  return $this->add_additional_fields_schema( $schema );
165
  }
166
 
167
+ /**
168
+ * Get the query params for collections
169
+ *
170
+ * @return array
171
+ */
172
+ public function get_collection_params() {
173
+ return array(
174
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
175
+ );
176
+ }
177
+
178
  }
lib/endpoints/class-wp-rest-post-types-controller.php CHANGED
@@ -8,23 +8,23 @@ class WP_REST_Post_Types_Controller extends WP_REST_Controller {
8
  public function register_routes() {
9
 
10
  register_rest_route( 'wp/v2', '/types', array(
11
- 'methods' => WP_REST_Server::READABLE,
12
- 'callback' => array( $this, 'get_items' ),
13
- 'args' => array(
14
- 'post_type' => array(
15
- 'sanitize_callback' => 'sanitize_key',
16
- ),
17
  ),
18
- ) );
19
-
20
- register_rest_route( 'wp/v2', '/types/schema', array(
21
- 'methods' => WP_REST_Server::READABLE,
22
- 'callback' => array( $this, 'get_public_item_schema' ),
23
  ) );
24
 
25
  register_rest_route( 'wp/v2', '/types/(?P<type>[\w-]+)', array(
26
- 'methods' => WP_REST_Server::READABLE,
27
- 'callback' => array( $this, 'get_item' ),
 
 
 
 
 
 
28
  ) );
29
  }
30
 
@@ -36,12 +36,12 @@ class WP_REST_Post_Types_Controller extends WP_REST_Controller {
36
  */
37
  public function get_items( $request ) {
38
  $data = array();
39
- foreach ( get_post_types( array( 'public' => true ), 'object' ) as $obj ) {
40
- $post_type = $this->prepare_item_for_response( $obj, $request );
41
- if ( is_wp_error( $post_type ) ) {
42
  continue;
43
  }
44
- $data[ $obj->name ] = $post_type;
 
45
  }
46
  return $data;
47
  }
@@ -57,6 +57,12 @@ class WP_REST_Post_Types_Controller extends WP_REST_Controller {
57
  if ( empty( $obj ) ) {
58
  return new WP_Error( 'rest_type_invalid', __( 'Invalid type.' ), array( 'status' => 404 ) );
59
  }
 
 
 
 
 
 
60
  return $this->prepare_item_for_response( $obj, $request );
61
  }
62
 
@@ -68,10 +74,6 @@ class WP_REST_Post_Types_Controller extends WP_REST_Controller {
68
  * @return array Post type data
69
  */
70
  public function prepare_item_for_response( $post_type, $request ) {
71
- if ( false === $post_type->public ) {
72
- return new WP_Error( 'rest_cannot_read_type', __( 'Cannot view type.' ), array( 'status' => 403 ) );
73
- }
74
-
75
  $data = array(
76
  'description' => $post_type->description,
77
  'hierarchical' => $post_type->hierarchical,
@@ -83,7 +85,29 @@ class WP_REST_Post_Types_Controller extends WP_REST_Controller {
83
  $data = $this->filter_response_by_context( $data, $context );
84
  $data = $this->add_additional_fields_to_object( $data, $request );
85
 
86
- return $data;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
  /**
@@ -98,33 +122,44 @@ class WP_REST_Post_Types_Controller extends WP_REST_Controller {
98
  'type' => 'object',
99
  'properties' => array(
100
  'description' => array(
101
- 'description' => 'A human-readable description of the object.',
102
  'type' => 'string',
103
- 'context' => array( 'view' ),
104
  ),
105
  'hierarchical' => array(
106
- 'description' => 'Whether or not the type should have children.',
107
  'type' => 'boolean',
108
- 'context' => array( 'view' ),
109
  ),
110
  'labels' => array(
111
- 'description' => 'Human-readable labels for the type for various contexts.',
112
  'type' => 'object',
113
- 'context' => array( 'view' ),
114
  ),
115
  'name' => array(
116
- 'description' => 'The title for the object.',
117
  'type' => 'string',
118
- 'context' => array( 'view' ),
119
  ),
120
  'slug' => array(
121
- 'description' => 'An alphanumeric identifier for the object.',
122
  'type' => 'string',
123
- 'context' => array( 'view' ),
124
  ),
125
  ),
126
  );
127
  return $this->add_additional_fields_schema( $schema );
128
  }
129
 
 
 
 
 
 
 
 
 
 
 
 
130
  }
8
  public function register_routes() {
9
 
10
  register_rest_route( 'wp/v2', '/types', array(
11
+ array(
12
+ 'methods' => WP_REST_Server::READABLE,
13
+ 'callback' => array( $this, 'get_items' ),
14
+ 'args' => $this->get_collection_params(),
 
 
15
  ),
16
+ 'schema' => array( $this, 'get_public_item_schema' ),
 
 
 
 
17
  ) );
18
 
19
  register_rest_route( 'wp/v2', '/types/(?P<type>[\w-]+)', array(
20
+ array(
21
+ 'methods' => WP_REST_Server::READABLE,
22
+ 'callback' => array( $this, 'get_item' ),
23
+ 'args' => array(
24
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
25
+ ),
26
+ ),
27
+ 'schema' => array( $this, 'get_public_item_schema' ),
28
  ) );
29
  }
30
 
36
  */
37
  public function get_items( $request ) {
38
  $data = array();
39
+ foreach ( get_post_types( array(), 'object' ) as $obj ) {
40
+ if ( empty( $obj->show_in_rest ) || ( 'edit' === $request['context'] && ! current_user_can( $obj->cap->edit_posts ) ) ) {
 
41
  continue;
42
  }
43
+ $post_type = $this->prepare_item_for_response( $obj, $request );
44
+ $data[ $obj->name ] = $this->prepare_response_for_collection( $post_type );
45
  }
46
  return $data;
47
  }
57
  if ( empty( $obj ) ) {
58
  return new WP_Error( 'rest_type_invalid', __( 'Invalid type.' ), array( 'status' => 404 ) );
59
  }
60
+ if ( empty( $obj->show_in_rest ) ) {
61
+ return new WP_Error( 'rest_cannot_read_type', __( 'Cannot view type.' ), array( 'status' => rest_authorization_required_code() ) );
62
+ }
63
+ if ( 'edit' === $request['context'] && ! current_user_can( $obj->cap->edit_posts ) ) {
64
+ return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to manage this type.' ), array( 'status' => rest_authorization_required_code() ) );
65
+ }
66
  return $this->prepare_item_for_response( $obj, $request );
67
  }
68
 
74
  * @return array Post type data
75
  */
76
  public function prepare_item_for_response( $post_type, $request ) {
 
 
 
 
77
  $data = array(
78
  'description' => $post_type->description,
79
  'hierarchical' => $post_type->hierarchical,
85
  $data = $this->filter_response_by_context( $data, $context );
86
  $data = $this->add_additional_fields_to_object( $data, $request );
87
 
88
+ // Wrap the data in a response object.
89
+ $response = rest_ensure_response( $data );
90
+
91
+ $base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
92
+ $response->add_links( array(
93
+ 'collection' => array(
94
+ 'href' => rest_url( 'wp/v2/types' ),
95
+ ),
96
+ 'https://api.w.org/items' => array(
97
+ 'href' => rest_url( sprintf( 'wp/v2/%s', $base ) ),
98
+ ),
99
+ ) );
100
+
101
+ /**
102
+ * Filter a post type returned from the API.
103
+ *
104
+ * Allows modification of the post type data right before it is returned.
105
+ *
106
+ * @param WP_REST_Response $response The response object.
107
+ * @param object $item The original post type object.
108
+ * @param WP_REST_Request $request Request used to generate the response.
109
+ */
110
+ return apply_filters( 'rest_prepare_post_type', $response, $post_type, $request );
111
  }
112
 
113
  /**
122
  'type' => 'object',
123
  'properties' => array(
124
  'description' => array(
125
+ 'description' => __( 'A human-readable description of the object.' ),
126
  'type' => 'string',
127
+ 'context' => array( 'view', 'edit' ),
128
  ),
129
  'hierarchical' => array(
130
+ 'description' => __( 'Whether or not the type should have children.' ),
131
  'type' => 'boolean',
132
+ 'context' => array( 'view', 'edit' ),
133
  ),
134
  'labels' => array(
135
+ 'description' => __( 'Human-readable labels for the type for various contexts.' ),
136
  'type' => 'object',
137
+ 'context' => array( 'edit' ),
138
  ),
139
  'name' => array(
140
+ 'description' => __( 'The title for the object.' ),
141
  'type' => 'string',
142
+ 'context' => array( 'view', 'edit' ),
143
  ),
144
  'slug' => array(
145
+ 'description' => __( 'An alphanumeric identifier for the object.' ),
146
  'type' => 'string',
147
+ 'context' => array( 'view', 'edit' ),
148
  ),
149
  ),
150
  );
151
  return $this->add_additional_fields_schema( $schema );
152
  }
153
 
154
+ /**
155
+ * Get the query params for collections
156
+ *
157
+ * @return array
158
+ */
159
+ public function get_collection_params() {
160
+ return array(
161
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
162
+ );
163
+ }
164
+
165
  }
lib/endpoints/class-wp-rest-posts-controller.php CHANGED
@@ -15,38 +15,21 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
15
 
16
  $base = $this->get_post_type_base( $this->post_type );
17
 
18
- $posts_args = array(
19
- 'context' => array(
20
- 'default' => 'view',
21
- ),
22
- 'page' => array(
23
- 'default' => 1,
24
- 'sanitize_callback' => 'absint',
25
- ),
26
- 'per_page' => array(
27
- 'default' => 10,
28
- 'sanitize_callback' => 'absint',
29
- ),
30
- );
31
-
32
- foreach ( $this->get_allowed_query_vars() as $var ) {
33
- if ( ! isset( $posts_args[ $var ] ) ) {
34
- $posts_args[ $var ] = array();
35
- }
36
- }
37
-
38
  register_rest_route( 'wp/v2', '/' . $base, array(
39
  array(
40
  'methods' => WP_REST_Server::READABLE,
41
  'callback' => array( $this, 'get_items' ),
42
- 'args' => $posts_args,
 
43
  ),
44
  array(
45
  'methods' => WP_REST_Server::CREATABLE,
46
  'callback' => array( $this, 'create_item' ),
47
  'permission_callback' => array( $this, 'create_item_permissions_check' ),
48
- 'args' => $this->get_endpoint_args_for_item_schema( true ),
49
  ),
 
 
50
  ) );
51
  register_rest_route( 'wp/v2', '/' . $base . '/(?P<id>[\d]+)', array(
52
  array(
@@ -54,16 +37,14 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
54
  'callback' => array( $this, 'get_item' ),
55
  'permission_callback' => array( $this, 'get_item_permissions_check' ),
56
  'args' => array(
57
- 'context' => array(
58
- 'default' => 'view',
59
- ),
60
  ),
61
  ),
62
  array(
63
  'methods' => WP_REST_Server::EDITABLE,
64
  'callback' => array( $this, 'update_item' ),
65
  'permission_callback' => array( $this, 'update_item_permissions_check' ),
66
- 'args' => $this->get_endpoint_args_for_item_schema( false ),
67
  ),
68
  array(
69
  'methods' => WP_REST_Server::DELETABLE,
@@ -75,34 +56,45 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
75
  ),
76
  ),
77
  ),
78
- ) );
79
- register_rest_route( 'wp/v2', '/' . $base . '/schema', array(
80
- 'methods' => WP_REST_Server::READABLE,
81
- 'callback' => array( $this, 'get_public_item_schema' ),
82
  ) );
83
  }
84
 
85
  /**
86
- * Get a collection of posts
87
  *
88
- * @param WP_REST_Request $request Full details about the request
89
  * @return WP_Error|WP_REST_Response
90
  */
91
  public function get_items( $request ) {
92
- $args = (array) $request->get_params();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  $args['post_type'] = $this->post_type;
94
- $args['paged'] = $args['page'];
95
- $args['posts_per_page'] = $args['per_page'];
96
- unset( $args['page'] );
97
 
98
  /**
99
- * Alter the query arguments for a request.
100
  *
101
- * This allows you to set extra arguments or defaults for a post
102
  * collection request.
103
  *
104
- * @param array $args Map of query var to query value.
105
- * @param WP_REST_Request $request Full details about the request.
106
  */
107
  $args = apply_filters( 'rest_post_query', $args, $request );
108
  $query_args = $this->prepare_items_query( $args );
@@ -122,24 +114,35 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
122
 
123
  $response = rest_ensure_response( $posts );
124
  $count_query = new WP_Query();
 
 
 
125
  unset( $query_args['paged'] );
 
126
  $query_result = $count_query->query( $query_args );
127
  $total_posts = $count_query->found_posts;
128
  $response->header( 'X-WP-Total', (int) $total_posts );
129
- $max_pages = ceil( $total_posts / $request['per_page'] );
130
  $response->header( 'X-WP-TotalPages', (int) $max_pages );
131
 
132
- $base = add_query_arg( $request->get_query_params(), rest_url( '/wp/v2/' . $this->get_post_type_base( $this->post_type ) ) );
133
- if ( $request['page'] > 1 ) {
134
- $prev_page = $request['page'] - 1;
 
 
 
 
 
 
 
135
  if ( $prev_page > $max_pages ) {
136
  $prev_page = $max_pages;
137
  }
138
  $prev_link = add_query_arg( 'page', $prev_page, $base );
139
  $response->link_header( 'prev', $prev_link );
140
  }
141
- if ( $max_pages > $request['page'] ) {
142
- $next_page = $request['page'] + 1;
143
  $next_link = add_query_arg( 'page', $next_page, $base );
144
  $response->link_header( 'next', $next_link );
145
  }
@@ -148,9 +151,9 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
148
  }
149
 
150
  /**
151
- * Get a single post
152
  *
153
- * @param WP_REST_Request $request Full details about the request
154
  * @return WP_Error|WP_REST_Response
155
  */
156
  public function get_item( $request ) {
@@ -158,7 +161,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
158
  $post = get_post( $id );
159
 
160
  if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
161
- return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
162
  }
163
 
164
  $data = $this->prepare_item_for_response( $post, $request );
@@ -170,9 +173,9 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
170
  }
171
 
172
  /**
173
- * Create a single post
174
  *
175
- * @param WP_REST_Request $request Full details about the request
176
  * @return WP_Error|WP_REST_Response
177
  */
178
  public function create_item( $request ) {
@@ -224,16 +227,18 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
224
  $this->update_additional_fields_for_object( get_post( $post_id ), $request );
225
 
226
  /**
227
- * @TODO: Enable rest_insert_post() action after
228
- * Media Controller has been migrated to new style.
229
  *
230
- * do_action( 'rest_insert_post', $post, $request, true );
 
 
231
  */
 
232
 
233
- $response = $this->get_item( array(
234
- 'id' => $post_id,
235
- 'context' => 'edit',
236
- ) );
237
  $response = rest_ensure_response( $response );
238
  $response->set_status( 201 );
239
  $response->header( 'Location', rest_url( '/wp/v2/' . $this->get_post_type_base( $post->post_type ) . '/' . $post_id ) );
@@ -242,25 +247,25 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
242
  }
243
 
244
  /**
245
- * Update a single post
246
  *
247
- * @param WP_REST_Request $request Full details about the request
248
  * @return WP_Error|WP_REST_Response
249
  */
250
  public function update_item( $request ) {
251
  $id = (int) $request['id'];
252
  $post = get_post( $id );
253
 
254
- if ( ! $post ) {
255
- return new WP_Error( 'rest_post_invalid_id', __( 'Post ID is invalid.' ), array( 'status' => 400 ) );
256
  }
257
 
258
  $post = $this->prepare_item_for_database( $request );
259
  if ( is_wp_error( $post ) ) {
260
  return $post;
261
  }
262
-
263
- $post_id = wp_update_post( $post, true );
264
  if ( is_wp_error( $post_id ) ) {
265
  if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) {
266
  $post_id->add_data( array( 'status' => 500 ) );
@@ -295,23 +300,27 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
295
  $this->update_additional_fields_for_object( get_post( $post_id ), $request );
296
 
297
  /**
298
- * @TODO: Enable rest_insert_post() action after
299
  * Media Controller has been migrated to new style.
300
  *
301
  * do_action( 'rest_insert_post', $post, $request );
302
  */
303
 
304
- return $this->get_item( array(
305
- 'id' => $post_id,
306
- 'context' => 'edit',
307
- ));
 
 
 
 
308
  }
309
 
310
  /**
311
- * Delete a single post
312
  *
313
- * @param WP_REST_Request $request Full details about the request
314
- * @return array|WP_Error
315
  */
316
  public function delete_item( $request ) {
317
  $id = (int) $request['id'];
@@ -320,7 +329,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
320
  $post = get_post( $id );
321
 
322
  if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
323
- return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
324
  }
325
 
326
  $supports_trash = ( EMPTY_TRASH_DAYS > 0 );
@@ -329,31 +338,34 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
329
  }
330
 
331
  /**
332
- * Filter whether the post type supports trashing.
 
 
333
  *
334
- * @param boolean $supports_trash Does the post type support trashing?
335
- * @param WP_Post $post Post we're attempting to trash.
336
  */
337
- $supports_trash = apply_filters( 'rest_post_type_trashable', $supports_trash, $post );
338
 
339
  if ( ! $this->check_delete_permission( $post ) ) {
340
- return new WP_Error( 'rest_user_cannot_delete_post', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => 401 ) );
341
  }
342
 
343
  $request = new WP_REST_Request( 'GET', '/wp/v2/' . $this->get_post_type_base( $this->post_type ) . '/' . $post->ID );
344
  $request->set_param( 'context', 'edit' );
345
  $response = rest_do_request( $request );
346
 
347
- // If we're forcing, then delete permanently
348
  if ( $force ) {
349
  $result = wp_delete_post( $id, true );
 
350
  } else {
351
- // If we don't support trashing for this type, error out
352
  if ( ! $supports_trash ) {
353
  return new WP_Error( 'rest_trash_not_supported', __( 'The post does not support trashing.' ), array( 'status' => 501 ) );
354
  }
355
 
356
- // Otherwise, only trash if we haven't already
357
  if ( 'trash' === $post->post_status ) {
358
  return new WP_Error( 'rest_already_deleted', __( 'The post has already been deleted.' ), array( 'status' => 410 ) );
359
  }
@@ -361,17 +373,51 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
361
  // (Note that internally this falls through to `wp_delete_post` if
362
  // the trash is disabled.)
363
  $result = wp_trash_post( $id );
 
364
  }
365
 
366
  if ( ! $result ) {
367
  return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
368
  }
369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  return $response;
371
  }
372
 
373
  /**
374
- * Check if a given request has access to read a post
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  *
376
  * @param WP_REST_Request $request Full details about the request.
377
  * @return bool|WP_Error
@@ -381,7 +427,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
381
  $post = get_post( (int) $request['id'] );
382
 
383
  if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
384
- return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to create password protected posts in this post type' ), array( 'status' => 403 ) );
385
  }
386
 
387
  if ( $post ) {
@@ -392,7 +438,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
392
  }
393
 
394
  /**
395
- * Check if a given request has access to create a post
396
  *
397
  * @param WP_REST_Request $request Full details about the request.
398
  * @return bool|WP_Error
@@ -402,22 +448,22 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
402
  $post_type = get_post_type_object( $this->post_type );
403
 
404
  if ( ! empty( $request['password'] ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
405
- return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to create password protected posts in this post type' ), array( 'status' => 403 ) );
406
  }
407
 
408
  if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
409
- return new WP_Error( 'rest_cannot_edit_others', __( 'You are not allowed to create posts as this user.' ), array( 'status' => 403 ) );
410
  }
411
 
412
  if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
413
- return new WP_Error( 'rest_cannot_assign_sticky', __( 'You do not have permission to make posts sticky.' ), array( 'status' => 403 ) );
414
  }
415
 
416
  return current_user_can( $post_type->cap->create_posts );
417
  }
418
 
419
  /**
420
- * Check if a given request has access to update a post
421
  *
422
  * @param WP_REST_Request $request Full details about the request.
423
  * @return bool|WP_Error
@@ -432,22 +478,22 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
432
  }
433
 
434
  if ( ! empty( $request['password'] ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
435
- return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to create password protected posts in this post type' ), array( 'status' => 403 ) );
436
  }
437
 
438
  if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
439
- return new WP_Error( 'rest_cannot_edit_others', __( 'You are not allowed to update posts as this user.' ), array( 'status' => 403 ) );
440
  }
441
 
442
  if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
443
- return new WP_Error( 'rest_cannot_assign_sticky', __( 'You do not have permission to make posts sticky.' ), array( 'status' => 403 ) );
444
  }
445
 
446
  return true;
447
  }
448
 
449
  /**
450
- * Check if a given request has access to delete a post
451
  *
452
  * @param WP_REST_Request $request Full details about the request.
453
  * @return bool|WP_Error
@@ -457,7 +503,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
457
  $post = get_post( $request['id'] );
458
 
459
  if ( $post && ! $this->check_delete_permission( $post ) ) {
460
- return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete posts.' ), array( 'status' => 403 ) );
461
  }
462
 
463
  return true;
@@ -476,7 +522,15 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
476
  $query_args = array();
477
  foreach ( $valid_vars as $var => $index ) {
478
  if ( isset( $prepared_args[ $var ] ) ) {
479
- $query_args[ $var ] = apply_filters( 'rest_query_var-' . $var, $prepared_args[ $var ] );
 
 
 
 
 
 
 
 
480
  }
481
  }
482
 
@@ -484,6 +538,14 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
484
  $query_args['post_status'] = 'inherit';
485
  }
486
 
 
 
 
 
 
 
 
 
487
  return $query_args;
488
  }
489
 
@@ -494,36 +556,50 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
494
  */
495
  protected function get_allowed_query_vars() {
496
  global $wp;
 
 
 
 
 
 
 
 
497
  $valid_vars = apply_filters( 'query_vars', $wp->public_query_vars );
498
 
499
- if ( current_user_can( 'edit_posts' ) ) {
 
500
  /**
501
- * Alter allowed query vars for authorized users.
502
  *
503
  * If the user has the `edit_posts` capability, we also allow use of
504
  * private query parameters, which are only undesirable on the
505
  * frontend, but are safe for use in query strings.
506
  *
507
  * To disable anyway, use
508
- * `add_filter('rest_private_query_vars', '__return_empty_array');`
509
  *
510
- * @param array $private List of allowed query vars for authorized users.
 
511
  */
512
  $private = apply_filters( 'rest_private_query_vars', $wp->private_query_vars );
513
  $valid_vars = array_merge( $valid_vars, $private );
514
  }
515
- // Define our own in addition to WP's normal vars
516
- $rest_valid = array( 'posts_per_page', 'ignore_sticky_posts', 'post_parent' );
517
  $valid_vars = array_merge( $valid_vars, $rest_valid );
518
 
519
  /**
520
- * Alter allowed query vars for the REST API.
521
  *
522
- * This filter allows you to add or remove query vars from the allowed
523
  * list for all requests, including unauthenticated ones. To alter the
524
  * vars for editors only, {@see rest_private_query_vars}.
525
  *
526
- * @param array $valid_vars List of allowed query vars.
 
 
 
 
527
  */
528
  $valid_vars = apply_filters( 'rest_query_vars', $valid_vars );
529
 
@@ -531,7 +607,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
531
  }
532
 
533
  /**
534
- * Check the post excerpt and prepare it for single post output
535
  *
536
  * @param string $excerpt
537
  * @return string|null $excerpt
@@ -541,6 +617,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
541
  return __( 'There is no excerpt because this is a protected post.' );
542
  }
543
 
 
544
  $excerpt = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $excerpt ) );
545
 
546
  if ( empty( $excerpt ) ) {
@@ -564,10 +641,10 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
564
  }
565
 
566
  if ( isset( $date ) ) {
567
- return rest_mysql_to_rfc3339( $date );
568
  }
569
 
570
- return rest_mysql_to_rfc3339( $date_gmt );
571
  }
572
 
573
  protected function prepare_password_response( $password ) {
@@ -586,22 +663,22 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
586
  }
587
 
588
  /**
589
- * Prepare a single post for create or update
590
  *
591
- * @param WP_REST_Request $request Request object
592
- * @return WP_Error|obj $prepared_post Post object
593
  */
594
  protected function prepare_item_for_database( $request ) {
595
  $prepared_post = new stdClass;
596
 
597
- // ID
598
  if ( isset( $request['id'] ) ) {
599
  $prepared_post->ID = absint( $request['id'] );
600
  }
601
 
602
  $schema = $this->get_item_schema();
603
 
604
- // Post title
605
  if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
606
  if ( is_string( $request['title'] ) ) {
607
  $prepared_post->post_title = wp_filter_post_kses( $request['title'] );
@@ -610,7 +687,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
610
  }
611
  }
612
 
613
- // Post content
614
  if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
615
  if ( is_string( $request['content'] ) ) {
616
  $prepared_post->post_content = wp_filter_post_kses( $request['content'] );
@@ -619,7 +696,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
619
  }
620
  }
621
 
622
- // Post excerpt
623
  if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) {
624
  if ( is_string( $request['excerpt'] ) ) {
625
  $prepared_post->post_excerpt = wp_filter_post_kses( $request['excerpt'] );
@@ -628,9 +705,9 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
628
  }
629
  }
630
 
631
- // Post type
632
  if ( empty( $request['id'] ) ) {
633
- // Creating new post, use default type for the controller
634
  $prepared_post->post_type = $this->post_type;
635
  } else {
636
  // Updating a post, use previous type.
@@ -638,7 +715,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
638
  }
639
  $post_type = get_post_type_object( $prepared_post->post_type );
640
 
641
- // Post status
642
  if ( isset( $request['status'] ) ) {
643
  $status = $this->handle_status_param( $request['status'], $post_type );
644
  if ( is_wp_error( $status ) ) {
@@ -648,27 +725,23 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
648
  $prepared_post->post_status = $status;
649
  }
650
 
651
- // Post date
652
  if ( ! empty( $request['date'] ) ) {
653
  $date_data = rest_get_date_with_gmt( $request['date'] );
654
 
655
  if ( ! empty( $date_data ) ) {
656
  list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
657
- } else {
658
- return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ), array( 'status' => 400 ) );
659
  }
660
  } elseif ( ! empty( $request['date_gmt'] ) ) {
661
  $date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
662
 
663
  if ( ! empty( $date_data ) ) {
664
  list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
665
- } else {
666
- return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ), array( 'status' => 400 ) );
667
  }
668
  }
669
- // Post slug
670
  if ( isset( $request['slug'] ) ) {
671
- $prepared_post->post_name = sanitize_title( $request['slug'] );
672
  }
673
 
674
  // Author
@@ -681,8 +754,8 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
681
  $prepared_post->post_author = $author;
682
  }
683
 
684
- // Post password
685
- if ( isset( $request['password'] ) ) {
686
  $prepared_post->post_password = $request['password'];
687
 
688
  if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
@@ -700,33 +773,43 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
700
  }
701
  }
702
 
703
- // Parent
704
  $post_type_obj = get_post_type_object( $this->post_type );
705
  if ( ! empty( $schema['properties']['parent'] ) && ! empty( $request['parent'] ) ) {
706
  $parent = get_post( (int) $request['parent'] );
707
  if ( empty( $parent ) ) {
708
- return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post parent ID.' ), array( 'status' => 400 ) );
709
  }
710
 
711
  $prepared_post->post_parent = (int) $parent->ID;
712
  }
713
 
714
- // Menu order
715
  if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) {
716
  $prepared_post->menu_order = (int) $request['menu_order'];
717
  }
718
 
719
- // Comment status
720
  if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) {
721
- $prepared_post->comment_status = sanitize_text_field( $request['comment_status'] );
722
  }
723
 
724
- // Ping status
725
  if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) {
726
- $prepared_post->ping_status = sanitize_text_field( $request['ping_status'] );
727
  }
 
 
 
 
 
 
 
 
 
 
 
728
 
729
- return apply_filters( 'rest_pre_insert_' . $this->post_type, $prepared_post, $request );
730
  }
731
 
732
  /**
@@ -737,7 +820,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
737
  * @return WP_Error|string $post_status
738
  */
739
  protected function handle_status_param( $post_status, $post_type ) {
740
- $post_status = sanitize_text_field( $post_status );
741
 
742
  switch ( $post_status ) {
743
  case 'draft':
@@ -745,13 +828,13 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
745
  break;
746
  case 'private':
747
  if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
748
- return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to create private posts in this post type' ), array( 'status' => 403 ) );
749
  }
750
  break;
751
  case 'publish':
752
  case 'future':
753
  if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
754
- return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to publish posts in this post type' ), array( 'status' => 403 ) );
755
  }
756
  break;
757
  default:
@@ -781,13 +864,13 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
781
  $post_author = (int) $post_author;
782
  }
783
 
784
- // Only check edit others' posts if we are another user
785
  if ( get_current_user_id() !== $post_author ) {
786
 
787
  $author = get_userdata( $post_author );
788
 
789
  if ( ! $author ) {
790
- return new WP_Error( 'rest_invalid_author', __( 'Invalid author ID.' ), array( 'status' => 400 ) );
791
  }
792
  }
793
 
@@ -795,7 +878,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
795
  }
796
 
797
  /**
798
- * Determine the featured image based on a request param
799
  *
800
  * @param int $featured_image
801
  * @param int $post_id
@@ -808,7 +891,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
808
  if ( $result ) {
809
  return true;
810
  } else {
811
- return new WP_Error( 'rest_invalid_featured_image', __( 'Invalid featured image ID.' ), array( 'status' => 400 ) );
812
  }
813
  } else {
814
  return delete_post_thumbnail( $post_id );
@@ -817,13 +900,13 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
817
  }
818
 
819
  /**
820
- * Set the template for a page
821
  *
822
  * @param string $template
823
  * @param integer $post_id
824
  */
825
  public function handle_template( $template, $post_id ) {
826
- if ( in_array( $template, array_values( get_page_templates() ) ) ) {
827
  update_post_meta( $post_id, '_wp_page_template', $template );
828
  } else {
829
  update_post_meta( $post_id, '_wp_page_template', '' );
@@ -841,7 +924,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
841
  $post_type = get_post_type_object( $post_type );
842
  }
843
 
844
- if ( ! empty( $post_type ) && $post_type->show_in_rest ) {
845
  return true;
846
  }
847
 
@@ -849,11 +932,11 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
849
  }
850
 
851
  /**
852
- * Check if we can read a post
853
  *
854
  * Correctly handles posts with the inherit status.
855
  *
856
- * @param obj $post Post object
857
  * @return bool Can we read it?
858
  */
859
  public function check_read_permission( $post ) {
@@ -871,6 +954,11 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
871
  return true;
872
  }
873
 
 
 
 
 
 
874
  // Can we read the parent if we're inheriting?
875
  if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
876
  $parent = get_post( $post->post_parent );
@@ -878,7 +966,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
878
  }
879
 
880
  // If we don't have a parent, but the status is set to inherit, assume
881
- // it's published (as per get_post_status())
882
  if ( 'inherit' === $post->post_status ) {
883
  return true;
884
  }
@@ -887,9 +975,9 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
887
  }
888
 
889
  /**
890
- * Check if we can edit a post
891
  *
892
- * @param obj $post Post object
893
  * @return bool Can we edit it?
894
  */
895
  protected function check_update_permission( $post ) {
@@ -903,10 +991,10 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
903
  }
904
 
905
  /**
906
- * Check if we can create a post
907
  *
908
- * @param obj $post Post object
909
- * @return bool Can we create it?
910
  */
911
  protected function check_create_permission( $post ) {
912
  $post_type = get_post_type_object( $post->post_type );
@@ -919,9 +1007,9 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
919
  }
920
 
921
  /**
922
- * Check if we can delete a post
923
  *
924
- * @param obj $post Post object
925
  * @return bool Can we delete it?
926
  */
927
  protected function check_delete_permission( $post ) {
@@ -951,22 +1039,23 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
951
  }
952
 
953
  /**
954
- * Prepare a single post output for response
955
  *
956
- * @param WP_Post $post Post object
957
- * @param WP_REST_Request $request Request object
958
  * @return WP_REST_Response $data
959
  */
960
  public function prepare_item_for_response( $post, $request ) {
961
  $GLOBALS['post'] = $post;
962
  setup_postdata( $post );
963
 
964
- // Base fields for every post
965
  $data = array(
966
  'id' => $post->ID,
967
  'date' => $this->prepare_date_response( $post->post_date_gmt, $post->post_date ),
968
  'date_gmt' => $this->prepare_date_response( $post->post_date_gmt ),
969
  'guid' => array(
 
970
  'rendered' => apply_filters( 'get_the_guid', $post->guid ),
971
  'raw' => $post->guid,
972
  ),
@@ -996,10 +1085,11 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
996
 
997
  $data['content'] = array(
998
  'raw' => $post->post_content,
 
999
  'rendered' => apply_filters( 'the_content', $post->post_content ),
1000
  );
1001
 
1002
- // Don't leave our cookie lying around: https://github.com/WP-API/WP-API/issues/1055
1003
  if ( ! empty( $post->post_password ) ) {
1004
  $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] = '';
1005
  }
@@ -1050,7 +1140,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1050
 
1051
  if ( ! empty( $schema['properties']['format'] ) ) {
1052
  $data['format'] = get_post_format( $post->ID );
1053
- // Fill in blank post format
1054
  if ( empty( $data['format'] ) ) {
1055
  $data['format'] = 'standard';
1056
  }
@@ -1061,12 +1151,22 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1061
 
1062
  $data = $this->add_additional_fields_to_object( $data, $request );
1063
 
1064
- // Wrap the data in a response object
1065
- $data = rest_ensure_response( $data );
1066
 
1067
- $data->add_links( $this->prepare_links( $post ) );
1068
 
1069
- return apply_filters( 'rest_prepare_' . $this->post_type, $data, $post, $request );
 
 
 
 
 
 
 
 
 
 
1070
  }
1071
 
1072
  /**
@@ -1081,10 +1181,13 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1081
  // Entity meta
1082
  $links = array(
1083
  'self' => array(
1084
- 'href' => rest_url( trailingslashit( $base ) . $post->ID ),
1085
  ),
1086
  'collection' => array(
1087
- 'href' => rest_url( $base ),
 
 
 
1088
  ),
1089
  );
1090
 
@@ -1098,7 +1201,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1098
 
1099
  if ( in_array( $post->post_type, array( 'post', 'page' ) ) || post_type_supports( $post->post_type, 'comments' ) ) {
1100
  $replies_url = rest_url( '/wp/v2/comments' );
1101
- $replies_url = add_query_arg( 'post_id', $post->ID, $replies_url );
1102
  $links['replies'] = array(
1103
  'href' => $replies_url,
1104
  'embeddable' => true,
@@ -1118,35 +1221,37 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1118
  );
1119
  }
1120
 
 
 
 
 
 
 
 
 
1121
  if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ) ) ) {
1122
  $attachments_url = rest_url( 'wp/v2/media' );
1123
- $attachments_url = add_query_arg( 'post_parent', $post->ID, $attachments_url );
1124
- $links['http://v2.wp-api.org/attachment'] = array(
1125
  'href' => $attachments_url,
1126
- 'embeddable' => true,
1127
  );
1128
  }
1129
 
1130
  $taxonomies = get_object_taxonomies( $post->post_type );
1131
  if ( ! empty( $taxonomies ) ) {
1132
- $links['http://v2.wp-api.org/term'] = array();
1133
 
1134
  foreach ( $taxonomies as $tax ) {
1135
  $taxonomy_obj = get_taxonomy( $tax );
1136
  // Skip taxonomies that are not public.
1137
- if ( false === $taxonomy_obj->public ) {
1138
  continue;
1139
  }
1140
 
1141
- if ( 'post_tag' === $tax ) {
1142
- $terms_url = rest_url( '/wp/v2/terms/tag' );
1143
- } else {
1144
- $terms_url = rest_url( '/wp/v2/terms/' . $tax );
1145
- }
1146
-
1147
- $terms_url = add_query_arg( 'post', $post->ID, $terms_url );
1148
 
1149
- $links['http://v2.wp-api.org/term'][] = array(
1150
  'href' => $terms_url,
1151
  'taxonomy' => $tax,
1152
  'embeddable' => true,
@@ -1155,7 +1260,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1155
  }
1156
 
1157
  if ( post_type_supports( $post->post_type, 'custom-fields' ) ) {
1158
- $links['http://v2.wp-api.org/meta'] = array(
1159
  'href' => rest_url( trailingslashit( $base ) . $post->ID . '/meta' ),
1160
  'embeddable' => true,
1161
  );
@@ -1165,7 +1270,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1165
  }
1166
 
1167
  /**
1168
- * Get the Post's schema, conforming to JSON Schema
1169
  *
1170
  * @return array
1171
  */
@@ -1177,82 +1282,87 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1177
  'title' => $this->post_type,
1178
  'type' => 'object',
1179
  /*
1180
- * Base properties for every Post
1181
  */
1182
  'properties' => array(
1183
  'date' => array(
1184
- 'description' => 'The date the object was published.',
1185
  'type' => 'string',
1186
  'format' => 'date-time',
1187
  'context' => array( 'view', 'edit', 'embed' ),
1188
  ),
1189
  'date_gmt' => array(
1190
- 'description' => 'The date the object was published, as GMT.',
1191
  'type' => 'string',
1192
  'format' => 'date-time',
1193
- 'context' => array( 'edit' ),
1194
  ),
1195
  'guid' => array(
1196
- 'description' => 'The globally unique identifier for the object.',
1197
  'type' => 'object',
1198
  'context' => array( 'view', 'edit' ),
1199
  'readonly' => true,
1200
  'properties' => array(
1201
  'raw' => array(
1202
- 'description' => 'GUID for the object, as it exists in the database.',
1203
  'type' => 'string',
1204
  'context' => array( 'edit' ),
1205
  ),
1206
  'rendered' => array(
1207
- 'description' => 'GUID for the object, transformed for display.',
1208
  'type' => 'string',
1209
  'context' => array( 'view', 'edit' ),
1210
  ),
1211
  ),
1212
  ),
1213
  'id' => array(
1214
- 'description' => 'Unique identifier for the object.',
1215
  'type' => 'integer',
1216
  'context' => array( 'view', 'edit', 'embed' ),
1217
  'readonly' => true,
1218
  ),
1219
  'link' => array(
1220
- 'description' => 'URL to the object.',
1221
  'type' => 'string',
1222
  'format' => 'uri',
1223
  'context' => array( 'view', 'edit', 'embed' ),
1224
  'readonly' => true,
1225
  ),
1226
  'modified' => array(
1227
- 'description' => 'The date the object was last modified.',
1228
  'type' => 'string',
1229
  'format' => 'date-time',
1230
  'context' => array( 'view', 'edit' ),
 
1231
  ),
1232
  'modified_gmt' => array(
1233
- 'description' => 'The date the object was last modified, as GMT.',
1234
  'type' => 'string',
1235
  'format' => 'date-time',
1236
  'context' => array( 'view', 'edit' ),
 
1237
  ),
1238
  'password' => array(
1239
- 'description' => 'A password to protect access to the post.',
1240
  'type' => 'string',
1241
  'context' => array( 'edit' ),
1242
  ),
1243
  'slug' => array(
1244
- 'description' => 'An alphanumeric identifier for the object unique to its type.',
1245
  'type' => 'string',
1246
  'context' => array( 'view', 'edit', 'embed' ),
 
 
 
1247
  ),
1248
  'status' => array(
1249
- 'description' => 'A named status for the object.',
1250
  'type' => 'string',
1251
  'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
1252
  'context' => array( 'edit' ),
1253
  ),
1254
  'type' => array(
1255
- 'description' => 'Type of Post for the object.',
1256
  'type' => 'string',
1257
  'context' => array( 'view', 'edit', 'embed' ),
1258
  'readonly' => true,
@@ -1263,7 +1373,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1263
  $post_type_obj = get_post_type_object( $this->post_type );
1264
  if ( $post_type_obj->hierarchical ) {
1265
  $schema['properties']['parent'] = array(
1266
- 'description' => 'The ID for the parent of the object.',
1267
  'type' => 'integer',
1268
  'context' => array( 'view', 'edit' ),
1269
  );
@@ -1319,17 +1429,17 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1319
 
1320
  case 'title':
1321
  $schema['properties']['title'] = array(
1322
- 'description' => 'The title for the object.',
1323
  'type' => 'object',
1324
  'context' => array( 'view', 'edit', 'embed' ),
1325
  'properties' => array(
1326
  'raw' => array(
1327
- 'description' => 'Title for the object, as it exists in the database.',
1328
  'type' => 'string',
1329
  'context' => array( 'edit' ),
1330
  ),
1331
  'rendered' => array(
1332
- 'description' => 'Title for the object, transformed for display.',
1333
  'type' => 'string',
1334
  'context' => array( 'view', 'edit', 'embed' ),
1335
  ),
@@ -1339,17 +1449,17 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1339
 
1340
  case 'editor':
1341
  $schema['properties']['content'] = array(
1342
- 'description' => 'The content for the object.',
1343
  'type' => 'object',
1344
  'context' => array( 'view', 'edit' ),
1345
  'properties' => array(
1346
  'raw' => array(
1347
- 'description' => 'Content for the object, as it exists in the database.',
1348
  'type' => 'string',
1349
  'context' => array( 'edit' ),
1350
  ),
1351
  'rendered' => array(
1352
- 'description' => 'Content for the object, transformed for display.',
1353
  'type' => 'string',
1354
  'context' => array( 'view', 'edit' ),
1355
  ),
@@ -1359,7 +1469,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1359
 
1360
  case 'author':
1361
  $schema['properties']['author'] = array(
1362
- 'description' => 'The ID for the author of the object.',
1363
  'type' => 'integer',
1364
  'context' => array( 'view', 'edit', 'embed' ),
1365
  );
@@ -1367,17 +1477,17 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1367
 
1368
  case 'excerpt':
1369
  $schema['properties']['excerpt'] = array(
1370
- 'description' => 'The excerpt for the object.',
1371
  'type' => 'object',
1372
  'context' => array( 'view', 'edit', 'embed' ),
1373
  'properties' => array(
1374
  'raw' => array(
1375
- 'description' => 'Excerpt for the object, as it exists in the database.',
1376
  'type' => 'string',
1377
  'context' => array( 'edit' ),
1378
  ),
1379
  'rendered' => array(
1380
- 'description' => 'Excerpt for the object, transformed for display.',
1381
  'type' => 'string',
1382
  'context' => array( 'view', 'edit', 'embed' ),
1383
  ),
@@ -1387,7 +1497,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1387
 
1388
  case 'thumbnail':
1389
  $schema['properties']['featured_image'] = array(
1390
- 'description' => 'ID of the featured image for the object.',
1391
  'type' => 'integer',
1392
  'context' => array( 'view', 'edit' ),
1393
  );
@@ -1395,13 +1505,13 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1395
 
1396
  case 'comments':
1397
  $schema['properties']['comment_status'] = array(
1398
- 'description' => 'Whether or not comments are open on the object.',
1399
  'type' => 'string',
1400
  'enum' => array( 'open', 'closed' ),
1401
  'context' => array( 'view', 'edit' ),
1402
  );
1403
  $schema['properties']['ping_status'] = array(
1404
- 'description' => 'Whether or not the object can be pinged.',
1405
  'type' => 'string',
1406
  'enum' => array( 'open', 'closed' ),
1407
  'context' => array( 'view', 'edit' ),
@@ -1410,7 +1520,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1410
 
1411
  case 'page-attributes':
1412
  $schema['properties']['menu_order'] = array(
1413
- 'description' => 'The order of the object in relation to other object of its type.',
1414
  'type' => 'integer',
1415
  'context' => array( 'view', 'edit' ),
1416
  );
@@ -1418,9 +1528,9 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1418
 
1419
  case 'post-formats':
1420
  $schema['properties']['format'] = array(
1421
- 'description' => 'The format for the object.',
1422
  'type' => 'string',
1423
- 'enum' => get_post_format_slugs(),
1424
  'context' => array( 'view', 'edit' ),
1425
  );
1426
  break;
@@ -1430,7 +1540,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1430
 
1431
  if ( 'post' === $this->post_type ) {
1432
  $schema['properties']['sticky'] = array(
1433
- 'description' => 'Whether or not the object should be treated as sticky.',
1434
  'type' => 'boolean',
1435
  'context' => array( 'view', 'edit' ),
1436
  );
@@ -1438,9 +1548,9 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1438
 
1439
  if ( 'page' === $this->post_type ) {
1440
  $schema['properties']['template'] = array(
1441
- 'description' => 'The theme file to use to display the object.',
1442
  'type' => 'string',
1443
- 'enum' => array_values( get_page_templates() ),
1444
  'context' => array( 'view', 'edit' ),
1445
  );
1446
  }
@@ -1448,4 +1558,89 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
1448
  return $this->add_additional_fields_schema( $schema );
1449
  }
1450
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1451
  }
15
 
16
  $base = $this->get_post_type_base( $this->post_type );
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  register_rest_route( 'wp/v2', '/' . $base, array(
19
  array(
20
  'methods' => WP_REST_Server::READABLE,
21
  'callback' => array( $this, 'get_items' ),
22
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
23
+ 'args' => $this->get_collection_params(),
24
  ),
25
  array(
26
  'methods' => WP_REST_Server::CREATABLE,
27
  'callback' => array( $this, 'create_item' ),
28
  'permission_callback' => array( $this, 'create_item_permissions_check' ),
29
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
30
  ),
31
+
32
+ 'schema' => array( $this, 'get_public_item_schema' ),
33
  ) );
34
  register_rest_route( 'wp/v2', '/' . $base . '/(?P<id>[\d]+)', array(
35
  array(
37
  'callback' => array( $this, 'get_item' ),
38
  'permission_callback' => array( $this, 'get_item_permissions_check' ),
39
  'args' => array(
40
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
 
 
41
  ),
42
  ),
43
  array(
44
  'methods' => WP_REST_Server::EDITABLE,
45
  'callback' => array( $this, 'update_item' ),
46
  'permission_callback' => array( $this, 'update_item_permissions_check' ),
47
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
48
  ),
49
  array(
50
  'methods' => WP_REST_Server::DELETABLE,
56
  ),
57
  ),
58
  ),
59
+
60
+ 'schema' => array( $this, 'get_public_item_schema' ),
 
 
61
  ) );
62
  }
63
 
64
  /**
65
+ * Get a collection of posts.
66
  *
67
+ * @param WP_REST_Request $request Full details about the request.
68
  * @return WP_Error|WP_REST_Response
69
  */
70
  public function get_items( $request ) {
71
+ $args = array();
72
+ $args['author'] = $request['author'];
73
+ $args['order'] = $request['order'];
74
+ $args['orderby'] = $request['orderby'];
75
+ $args['paged'] = $request['page'];
76
+ $args['post__in'] = $request['include'];
77
+ $args['posts_per_page'] = $request['per_page'];
78
+ $args['post_parent'] = $request['parent'];
79
+ $args['post_status'] = $request['status'];
80
+ $args['s'] = $request['search'];
81
+
82
+ if ( is_array( $request['filter'] ) ) {
83
+ $args = array_merge( $args, $request['filter'] );
84
+ unset( $args['filter'] );
85
+ }
86
+
87
+ // Force the post_type argument, since it's not a user input variable.
88
  $args['post_type'] = $this->post_type;
 
 
 
89
 
90
  /**
91
+ * Filter the query arguments for a request.
92
  *
93
+ * Enables adding extra arguments or setting defaults for a post
94
  * collection request.
95
  *
96
+ * @param array $args Key value array of query var to query value.
97
+ * @param WP_REST_Request $request The request used.
98
  */
99
  $args = apply_filters( 'rest_post_query', $args, $request );
100
  $query_args = $this->prepare_items_query( $args );
114
 
115
  $response = rest_ensure_response( $posts );
116
  $count_query = new WP_Query();
117
+
118
+ // Store paged value for pagination headers then unset for count query.
119
+ $page = (int) $query_args['paged'];
120
  unset( $query_args['paged'] );
121
+
122
  $query_result = $count_query->query( $query_args );
123
  $total_posts = $count_query->found_posts;
124
  $response->header( 'X-WP-Total', (int) $total_posts );
125
+ $max_pages = ceil( $total_posts / (int) $query_args['posts_per_page'] );
126
  $response->header( 'X-WP-TotalPages', (int) $max_pages );
127
 
128
+ $request_params = $request->get_query_params();
129
+ if ( ! empty( $request_params['filter'] ) ) {
130
+ // Normalize the pagination params.
131
+ unset( $request_params['filter']['posts_per_page'] );
132
+ unset( $request_params['filter']['paged'] );
133
+ }
134
+ $base = add_query_arg( $request_params, rest_url( '/wp/v2/' . $this->get_post_type_base( $this->post_type ) ) );
135
+
136
+ if ( $page > 1 ) {
137
+ $prev_page = $page - 1;
138
  if ( $prev_page > $max_pages ) {
139
  $prev_page = $max_pages;
140
  }
141
  $prev_link = add_query_arg( 'page', $prev_page, $base );
142
  $response->link_header( 'prev', $prev_link );
143
  }
144
+ if ( $max_pages > $page ) {
145
+ $next_page = $page + 1;
146
  $next_link = add_query_arg( 'page', $next_page, $base );
147
  $response->link_header( 'next', $next_link );
148
  }
151
  }
152
 
153
  /**
154
+ * Get a single post.
155
  *
156
+ * @param WP_REST_Request $request Full details about the request.
157
  * @return WP_Error|WP_REST_Response
158
  */
159
  public function get_item( $request ) {
161
  $post = get_post( $id );
162
 
163
  if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
164
+ return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
165
  }
166
 
167
  $data = $this->prepare_item_for_response( $post, $request );
173
  }
174
 
175
  /**
176
+ * Create a single post.
177
  *
178
+ * @param WP_REST_Request $request Full details about the request.
179
  * @return WP_Error|WP_REST_Response
180
  */
181
  public function create_item( $request ) {
227
  $this->update_additional_fields_for_object( get_post( $post_id ), $request );
228
 
229
  /**
230
+ * Fires after a single post is created or updated via the REST API.
 
231
  *
232
+ * @param object $post Inserted Post object (not a WP_Post object).
233
+ * @param WP_REST_Request $request Request object.
234
+ * @param bool $creating True when creating post, false when updating.
235
  */
236
+ do_action( 'rest_insert_post', $post, $request, true );
237
 
238
+ $get_request = new WP_REST_Request;
239
+ $get_request->set_param( 'id', $post_id );
240
+ $get_request->set_param( 'context', 'edit' );
241
+ $response = $this->get_item( $get_request );
242
  $response = rest_ensure_response( $response );
243
  $response->set_status( 201 );
244
  $response->header( 'Location', rest_url( '/wp/v2/' . $this->get_post_type_base( $post->post_type ) . '/' . $post_id ) );
247
  }
248
 
249
  /**
250
+ * Update a single post.
251
  *
252
+ * @param WP_REST_Request $request Full details about the request.
253
  * @return WP_Error|WP_REST_Response
254
  */
255
  public function update_item( $request ) {
256
  $id = (int) $request['id'];
257
  $post = get_post( $id );
258
 
259
+ if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
260
+ return new WP_Error( 'rest_post_invalid_id', __( 'Post id is invalid.' ), array( 'status' => 400 ) );
261
  }
262
 
263
  $post = $this->prepare_item_for_database( $request );
264
  if ( is_wp_error( $post ) ) {
265
  return $post;
266
  }
267
+ // convert the post object to an array, otherwise wp_update_post will expect non-escaped input
268
+ $post_id = wp_update_post( (array) $post, true );
269
  if ( is_wp_error( $post_id ) ) {
270
  if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) {
271
  $post_id->add_data( array( 'status' => 500 ) );
300
  $this->update_additional_fields_for_object( get_post( $post_id ), $request );
301
 
302
  /**
303
+ * @TODO: Enable rest_insert_post() action after.
304
  * Media Controller has been migrated to new style.
305
  *
306
  * do_action( 'rest_insert_post', $post, $request );
307
  */
308
 
309
+ /* This action is documented in lib/endpoints/class-wp-rest-controller.php */
310
+ do_action( 'rest_insert_post', $post, $request, false );
311
+
312
+ $get_request = new WP_REST_Request;
313
+ $get_request->set_param( 'id', $post_id );
314
+ $get_request->set_param( 'context', 'edit' );
315
+ $response = $this->get_item( $get_request );
316
+ return rest_ensure_response( $response );
317
  }
318
 
319
  /**
320
+ * Delete a single post.
321
  *
322
+ * @param WP_REST_Request $request Full details about the request.
323
+ * @return WP_REST_Response|WP_Error
324
  */
325
  public function delete_item( $request ) {
326
  $id = (int) $request['id'];
329
  $post = get_post( $id );
330
 
331
  if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
332
+ return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
333
  }
334
 
335
  $supports_trash = ( EMPTY_TRASH_DAYS > 0 );
338
  }
339
 
340
  /**
341
+ * Filter whether a post is trashable.
342
+ *
343
+ * Return false to disable trash support for the post.
344
  *
345
+ * @param boolean $supports_trash Whether the post type support trashing.
346
+ * @param WP_Post $post The Post object being considered for trashing support.
347
  */
348
+ $supports_trash = apply_filters( 'rest_post_trashable', $supports_trash, $post );
349
 
350
  if ( ! $this->check_delete_permission( $post ) ) {
351
+ return new WP_Error( 'rest_user_cannot_delete_post', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => rest_authorization_required_code() ) );
352
  }
353
 
354
  $request = new WP_REST_Request( 'GET', '/wp/v2/' . $this->get_post_type_base( $this->post_type ) . '/' . $post->ID );
355
  $request->set_param( 'context', 'edit' );
356
  $response = rest_do_request( $request );
357
 
358
+ // If we're forcing, then delete permanently.
359
  if ( $force ) {
360
  $result = wp_delete_post( $id, true );
361
+ $status = 'deleted';
362
  } else {
363
+ // If we don't support trashing for this type, error out.
364
  if ( ! $supports_trash ) {
365
  return new WP_Error( 'rest_trash_not_supported', __( 'The post does not support trashing.' ), array( 'status' => 501 ) );
366
  }
367
 
368
+ // Otherwise, only trash if we haven't already.
369
  if ( 'trash' === $post->post_status ) {
370
  return new WP_Error( 'rest_already_deleted', __( 'The post has already been deleted.' ), array( 'status' => 410 ) );
371
  }
373
  // (Note that internally this falls through to `wp_delete_post` if
374
  // the trash is disabled.)
375
  $result = wp_trash_post( $id );
376
+ $status = 'trashed';
377
  }
378
 
379
  if ( ! $result ) {
380
  return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
381
  }
382
 
383
+ $data = $response->get_data();
384
+ $data = array(
385
+ 'data' => $data,
386
+ $status => true,
387
+ );
388
+ $response->set_data( $data );
389
+
390
+ /**
391
+ * Fires after a single post is deleted or trashed via the REST API.
392
+ *
393
+ * @param object $post The deleted or trashed post.
394
+ * @param array $data The response data.
395
+ * @param WP_REST_Request $request The request sent to the API.
396
+ */
397
+ do_action( 'rest_delete_post', $post, $data, $request );
398
+
399
  return $response;
400
  }
401
 
402
  /**
403
+ * Check if a given request has access to read /posts.
404
+ *
405
+ * @param WP_REST_Request $request Full details about the request.
406
+ * @return bool|WP_Error
407
+ */
408
+ public function get_items_permissions_check( $request ) {
409
+
410
+ $post_type = get_post_type_object( $this->post_type );
411
+
412
+ if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
413
+ return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit these posts in this post type' ), array( 'status' => rest_authorization_required_code() ) );
414
+ }
415
+
416
+ return true;
417
+ }
418
+
419
+ /**
420
+ * Check if a given request has access to read a post.
421
  *
422
  * @param WP_REST_Request $request Full details about the request.
423
  * @return bool|WP_Error
427
  $post = get_post( (int) $request['id'] );
428
 
429
  if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
430
+ return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit this post' ), array( 'status' => rest_authorization_required_code() ) );
431
  }
432
 
433
  if ( $post ) {
438
  }
439
 
440
  /**
441
+ * Check if a given request has access to create a post.
442
  *
443
  * @param WP_REST_Request $request Full details about the request.
444
  * @return bool|WP_Error
448
  $post_type = get_post_type_object( $this->post_type );
449
 
450
  if ( ! empty( $request['password'] ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
451
+ return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to create password protected posts in this post type' ), array( 'status' => rest_authorization_required_code() ) );
452
  }
453
 
454
  if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
455
+ return new WP_Error( 'rest_cannot_edit_others', __( 'You are not allowed to create posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
456
  }
457
 
458
  if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
459
+ return new WP_Error( 'rest_cannot_assign_sticky', __( 'You do not have permission to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
460
  }
461
 
462
  return current_user_can( $post_type->cap->create_posts );
463
  }
464
 
465
  /**
466
+ * Check if a given request has access to update a post.
467
  *
468
  * @param WP_REST_Request $request Full details about the request.
469
  * @return bool|WP_Error
478
  }
479
 
480
  if ( ! empty( $request['password'] ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
481
+ return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to create password protected posts in this post type' ), array( 'status' => rest_authorization_required_code() ) );
482
  }
483
 
484
  if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
485
+ return new WP_Error( 'rest_cannot_edit_others', __( 'You are not allowed to update posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
486
  }
487
 
488
  if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
489
+ return new WP_Error( 'rest_cannot_assign_sticky', __( 'You do not have permission to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
490
  }
491
 
492
  return true;
493
  }
494
 
495
  /**
496
+ * Check if a given request has access to delete a post.
497
  *
498
  * @param WP_REST_Request $request Full details about the request.
499
  * @return bool|WP_Error
503
  $post = get_post( $request['id'] );
504
 
505
  if ( $post && ! $this->check_delete_permission( $post ) ) {
506
+ return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete posts.' ), array( 'status' => rest_authorization_required_code() ) );
507
  }
508
 
509
  return true;
522
  $query_args = array();
523
  foreach ( $valid_vars as $var => $index ) {
524
  if ( isset( $prepared_args[ $var ] ) ) {
525
+ /**
526
+ * Filter the query_vars used in `get_items` for the constructed query.
527
+ *
528
+ * The dynamic portion of the hook name, $var, refers to the query_var key.
529
+ *
530
+ * @param mixed $prepared_args[ $var ] The query_var value.
531
+ *
532
+ */
533
+ $query_args[ $var ] = apply_filters( "rest_query_var-{$var}", $prepared_args[ $var ] );
534
  }
535
  }
536
 
538
  $query_args['post_status'] = 'inherit';
539
  }
540
 
541
+ if ( 'post' !== $this->post_type || ! isset( $query_args['ignore_sticky_posts'] ) ) {
542
+ $query_args['ignore_sticky_posts'] = true;
543
+ }
544
+
545
+ if ( 'include' === $query_args['orderby'] ) {
546
+ $query_args['orderby'] = 'post__in';
547
+ }
548
+
549
  return $query_args;
550
  }
551
 
556
  */
557
  protected function get_allowed_query_vars() {
558
  global $wp;
559
+
560
+ /**
561
+ * Filter the publicly allowed query vars.
562
+ *
563
+ * Allows adjusting of the default query vars that are made public.
564
+ *
565
+ * @param array Array of allowed WP_Query query vars.
566
+ */
567
  $valid_vars = apply_filters( 'query_vars', $wp->public_query_vars );
568
 
569
+ $post_type_obj = get_post_type_object( $this->post_type );
570
+ if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
571
  /**
572
+ * Filter the allowed 'private' query vars for authorized users.
573
  *
574
  * If the user has the `edit_posts` capability, we also allow use of
575
  * private query parameters, which are only undesirable on the
576
  * frontend, but are safe for use in query strings.
577
  *
578
  * To disable anyway, use
579
+ * `add_filter( 'rest_private_query_vars', '__return_empty_array' );`
580
  *
581
+ * @param array $private_query_vars Array of allowed query vars for authorized users.
582
+ * }
583
  */
584
  $private = apply_filters( 'rest_private_query_vars', $wp->private_query_vars );
585
  $valid_vars = array_merge( $valid_vars, $private );
586
  }
587
+ // Define our own in addition to WP's normal vars.
588
+ $rest_valid = array( 'post__in', 'posts_per_page', 'ignore_sticky_posts', 'post_parent' );
589
  $valid_vars = array_merge( $valid_vars, $rest_valid );
590
 
591
  /**
592
+ * Filter allowed query vars for the REST API.
593
  *
594
+ * This filter allows you to add or remove query vars from the final allowed
595
  * list for all requests, including unauthenticated ones. To alter the
596
  * vars for editors only, {@see rest_private_query_vars}.
597
  *
598
+ * @param array {
599
+ * Array of allowed WP_Query query vars.
600
+ *
601
+ * @param string $allowed_query_var The query var to allow.
602
+ * }
603
  */
604
  $valid_vars = apply_filters( 'rest_query_vars', $valid_vars );
605
 
607
  }
608
 
609
  /**
610
+ * Check the post excerpt and prepare it for single post output.
611
  *
612
  * @param string $excerpt
613
  * @return string|null $excerpt
617
  return __( 'There is no excerpt because this is a protected post.' );
618
  }
619
 
620
+ /** This filter is documented in wp-includes/post-template.php */
621
  $excerpt = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $excerpt ) );
622
 
623
  if ( empty( $excerpt ) ) {
641
  }
642
 
643
  if ( isset( $date ) ) {
644
+ return mysql_to_rfc3339( $date );
645
  }
646
 
647
+ return mysql_to_rfc3339( $date_gmt );
648
  }
649
 
650
  protected function prepare_password_response( $password ) {
663
  }
664
 
665
  /**
666
+ * Prepare a single post for create or update.
667
  *
668
+ * @param WP_REST_Request $request Request object.
669
+ * @return WP_Error|object $prepared_post Post object.
670
  */
671
  protected function prepare_item_for_database( $request ) {
672
  $prepared_post = new stdClass;
673
 
674
+ // ID.
675
  if ( isset( $request['id'] ) ) {
676
  $prepared_post->ID = absint( $request['id'] );
677
  }
678
 
679
  $schema = $this->get_item_schema();
680
 
681
+ // Post title.
682
  if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
683
  if ( is_string( $request['title'] ) ) {
684
  $prepared_post->post_title = wp_filter_post_kses( $request['title'] );
687
  }
688
  }
689
 
690
+ // Post content.
691
  if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
692
  if ( is_string( $request['content'] ) ) {
693
  $prepared_post->post_content = wp_filter_post_kses( $request['content'] );
696
  }
697
  }
698
 
699
+ // Post excerpt.
700
  if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) {
701
  if ( is_string( $request['excerpt'] ) ) {
702
  $prepared_post->post_excerpt = wp_filter_post_kses( $request['excerpt'] );
705
  }
706
  }
707
 
708
+ // Post type.
709
  if ( empty( $request['id'] ) ) {
710
+ // Creating new post, use default type for the controller.
711
  $prepared_post->post_type = $this->post_type;
712
  } else {
713
  // Updating a post, use previous type.
715
  }
716
  $post_type = get_post_type_object( $prepared_post->post_type );
717
 
718
+ // Post status.
719
  if ( isset( $request['status'] ) ) {
720
  $status = $this->handle_status_param( $request['status'], $post_type );
721
  if ( is_wp_error( $status ) ) {
725
  $prepared_post->post_status = $status;
726
  }
727
 
728
+ // Post date.
729
  if ( ! empty( $request['date'] ) ) {
730
  $date_data = rest_get_date_with_gmt( $request['date'] );
731
 
732
  if ( ! empty( $date_data ) ) {
733
  list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
 
 
734
  }
735
  } elseif ( ! empty( $request['date_gmt'] ) ) {
736
  $date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
737
 
738
  if ( ! empty( $date_data ) ) {
739
  list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
 
 
740
  }
741
  }
742
+ // Post slug.
743
  if ( isset( $request['slug'] ) ) {
744
+ $prepared_post->post_name = $request['slug'];
745
  }
746
 
747
  // Author
754
  $prepared_post->post_author = $author;
755
  }
756
 
757
+ // Post password.
758
+ if ( isset( $request['password'] ) && '' !== $request['password'] ) {
759
  $prepared_post->post_password = $request['password'];
760
 
761
  if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
773
  }
774
  }
775
 
776
+ // Parent.
777
  $post_type_obj = get_post_type_object( $this->post_type );
778
  if ( ! empty( $schema['properties']['parent'] ) && ! empty( $request['parent'] ) ) {
779
  $parent = get_post( (int) $request['parent'] );
780
  if ( empty( $parent ) ) {
781
+ return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post parent id.' ), array( 'status' => 400 ) );
782
  }
783
 
784
  $prepared_post->post_parent = (int) $parent->ID;
785
  }
786
 
787
+ // Menu order.
788
  if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) {
789
  $prepared_post->menu_order = (int) $request['menu_order'];
790
  }
791
 
792
+ // Comment status.
793
  if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) {
794
+ $prepared_post->comment_status = $request['comment_status'];
795
  }
796
 
797
+ // Ping status.
798
  if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) {
799
+ $prepared_post->ping_status = $request['ping_status'];
800
  }
801
+ /**
802
+ * Filter the query_vars used in `get_items` for the constructed query.
803
+ *
804
+ * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
805
+ * prepared for insertion.
806
+ *
807
+ * @param object $prepared_post An object representing a single post prepared
808
+ * for inserting or updating the database.
809
+ * @param WP_REST_Request $request Request object.
810
+ */
811
+ return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
812
 
 
813
  }
814
 
815
  /**
820
  * @return WP_Error|string $post_status
821
  */
822
  protected function handle_status_param( $post_status, $post_type ) {
823
+ $post_status = $post_status;
824
 
825
  switch ( $post_status ) {
826
  case 'draft':
828
  break;
829
  case 'private':
830
  if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
831
+ return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to create private posts in this post type' ), array( 'status' => rest_authorization_required_code() ) );
832
  }
833
  break;
834
  case 'publish':
835
  case 'future':
836
  if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
837
+ return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to publish posts in this post type' ), array( 'status' => rest_authorization_required_code() ) );
838
  }
839
  break;
840
  default:
864
  $post_author = (int) $post_author;
865
  }
866
 
867
+ // Only check edit others' posts if we are another user.
868
  if ( get_current_user_id() !== $post_author ) {
869
 
870
  $author = get_userdata( $post_author );
871
 
872
  if ( ! $author ) {
873
+ return new WP_Error( 'rest_invalid_author', __( 'Invalid author id.' ), array( 'status' => 400 ) );
874
  }
875
  }
876
 
878
  }
879
 
880
  /**
881
+ * Determine the featured image based on a request param.
882
  *
883
  * @param int $featured_image
884
  * @param int $post_id
891
  if ( $result ) {
892
  return true;
893
  } else {
894
+ return new WP_Error( 'rest_invalid_featured_image', __( 'Invalid featured image id.' ), array( 'status' => 400 ) );
895
  }
896
  } else {
897
  return delete_post_thumbnail( $post_id );
900
  }
901
 
902
  /**
903
+ * Set the template for a page.
904
  *
905
  * @param string $template
906
  * @param integer $post_id
907
  */
908
  public function handle_template( $template, $post_id ) {
909
+ if ( in_array( $template, array_keys( wp_get_theme()->get_page_templates( get_post( $post_id ) ) ) ) ) {
910
  update_post_meta( $post_id, '_wp_page_template', $template );
911
  } else {
912
  update_post_meta( $post_id, '_wp_page_template', '' );
924
  $post_type = get_post_type_object( $post_type );
925
  }
926
 
927
+ if ( ! empty( $post_type ) && ! empty( $post_type->show_in_rest ) ) {
928
  return true;
929
  }
930
 
932
  }
933
 
934
  /**
935
+ * Check if we can read a post.
936
  *
937
  * Correctly handles posts with the inherit status.
938
  *
939
+ * @param object $post Post object.
940
  * @return bool Can we read it?
941
  */
942
  public function check_read_permission( $post ) {
954
  return true;
955
  }
956
 
957
+ $post_status_obj = get_post_status_object( $post->post_status );
958
+ if ( $post_status_obj && $post_status_obj->public ) {
959
+ return true;
960
+ }
961
+
962
  // Can we read the parent if we're inheriting?
963
  if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
964
  $parent = get_post( $post->post_parent );
966
  }
967
 
968
  // If we don't have a parent, but the status is set to inherit, assume
969
+ // it's published (as per get_post_status()).
970
  if ( 'inherit' === $post->post_status ) {
971
  return true;
972
  }
975
  }
976
 
977
  /**
978
+ * Check if we can edit a post.
979
  *
980
+ * @param object $post Post object.
981
  * @return bool Can we edit it?
982
  */
983
  protected function check_update_permission( $post ) {
991
  }
992
 
993
  /**
994
+ * Check if we can create a post.
995
  *
996
+ * @param object $post Post object.
997
+ * @return bool Can we create it?.
998
  */
999
  protected function check_create_permission( $post ) {
1000
  $post_type = get_post_type_object( $post->post_type );
1007
  }
1008
 
1009
  /**
1010
+ * Check if we can delete a post.
1011
  *
1012
+ * @param object $post Post object.
1013
  * @return bool Can we delete it?
1014
  */
1015
  protected function check_delete_permission( $post ) {
1039
  }
1040
 
1041
  /**
1042
+ * Prepare a single post output for response.
1043
  *
1044
+ * @param WP_Post $post Post object.
1045
+ * @param WP_REST_Request $request Request object.
1046
  * @return WP_REST_Response $data
1047
  */
1048
  public function prepare_item_for_response( $post, $request ) {
1049
  $GLOBALS['post'] = $post;
1050
  setup_postdata( $post );
1051
 
1052
+ // Base fields for every post.
1053
  $data = array(
1054
  'id' => $post->ID,
1055
  'date' => $this->prepare_date_response( $post->post_date_gmt, $post->post_date ),
1056
  'date_gmt' => $this->prepare_date_response( $post->post_date_gmt ),
1057
  'guid' => array(
1058
+ /** This filter is documented in wp-includes/post-template.php */
1059
  'rendered' => apply_filters( 'get_the_guid', $post->guid ),
1060
  'raw' => $post->guid,
1061
  ),
1085
 
1086
  $data['content'] = array(
1087
  'raw' => $post->post_content,
1088
+ /** This filter is documented in wp-includes/post-template.php */
1089
  'rendered' => apply_filters( 'the_content', $post->post_content ),
1090
  );
1091
 
1092
+ // Don't leave our cookie lying around: https://github.com/WP-API/WP-API/issues/1055.
1093
  if ( ! empty( $post->post_password ) ) {
1094
  $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] = '';
1095
  }
1140
 
1141
  if ( ! empty( $schema['properties']['format'] ) ) {
1142
  $data['format'] = get_post_format( $post->ID );
1143
+ // Fill in blank post format.
1144
  if ( empty( $data['format'] ) ) {
1145
  $data['format'] = 'standard';
1146
  }
1151
 
1152
  $data = $this->add_additional_fields_to_object( $data, $request );
1153
 
1154
+ // Wrap the data in a response object.
1155
+ $response = rest_ensure_response( $data );
1156
 
1157
+ $response->add_links( $this->prepare_links( $post ) );
1158
 
1159
+ /**
1160
+ * Filter the post data for a response.
1161
+ *
1162
+ * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
1163
+ * prepared for the response.
1164
+ *
1165
+ * @param WP_REST_Response $response The response object.
1166
+ * @param WP_Post $post Post object.
1167
+ * @param WP_REST_Request $request Request object.
1168
+ */
1169
+ return apply_filters( 'rest_prepare_' . $this->post_type, $response, $post, $request );
1170
  }
1171
 
1172
  /**
1181
  // Entity meta
1182
  $links = array(
1183
  'self' => array(
1184
+ 'href' => rest_url( trailingslashit( $base ) . $post->ID ),
1185
  ),
1186
  'collection' => array(
1187
+ 'href' => rest_url( $base ),
1188
+ ),
1189
+ 'about' => array(
1190
+ 'href' => rest_url( '/wp/v2/types/' . $this->post_type ),
1191
  ),
1192
  );
1193
 
1201
 
1202
  if ( in_array( $post->post_type, array( 'post', 'page' ) ) || post_type_supports( $post->post_type, 'comments' ) ) {
1203
  $replies_url = rest_url( '/wp/v2/comments' );
1204
+ $replies_url = add_query_arg( 'post', $post->ID, $replies_url );
1205
  $links['replies'] = array(
1206
  'href' => $replies_url,
1207
  'embeddable' => true,
1221
  );
1222
  }
1223
 
1224
+ // If we have a featured image, add that.
1225
+ if ( $featured_image = get_post_thumbnail_id( $post->ID ) ) {
1226
+ $image_url = rest_url( 'wp/v2/media/' . $featured_image );
1227
+ $links['https://api.w.org/featuredmedia'] = array(
1228
+ 'href' => $image_url,
1229
+ 'embeddable' => true,
1230
+ );
1231
+ }
1232
  if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ) ) ) {
1233
  $attachments_url = rest_url( 'wp/v2/media' );
1234
+ $attachments_url = add_query_arg( 'parent', $post->ID, $attachments_url );
1235
+ $links['https://api.w.org/attachment'] = array(
1236
  'href' => $attachments_url,
 
1237
  );
1238
  }
1239
 
1240
  $taxonomies = get_object_taxonomies( $post->post_type );
1241
  if ( ! empty( $taxonomies ) ) {
1242
+ $links['https://api.w.org/term'] = array();
1243
 
1244
  foreach ( $taxonomies as $tax ) {
1245
  $taxonomy_obj = get_taxonomy( $tax );
1246
  // Skip taxonomies that are not public.
1247
+ if ( false === $taxonomy_obj->public || 'post_format' === $tax ) {
1248
  continue;
1249
  }
1250
 
1251
+ $tax_base = ! empty( $taxonomy_obj->rest_base ) ? $taxonomy_obj->rest_base : $tax;
1252
+ $terms_url = rest_url( trailingslashit( $base ) . $post->ID . '/' . $tax_base );
 
 
 
 
 
1253
 
1254
+ $links['https://api.w.org/term'][] = array(
1255
  'href' => $terms_url,
1256
  'taxonomy' => $tax,
1257
  'embeddable' => true,
1260
  }
1261
 
1262
  if ( post_type_supports( $post->post_type, 'custom-fields' ) ) {
1263
+ $links['https://api.w.org/meta'] = array(
1264
  'href' => rest_url( trailingslashit( $base ) . $post->ID . '/meta' ),
1265
  'embeddable' => true,
1266
  );
1270
  }
1271
 
1272
  /**
1273
+ * Get the Post's schema, conforming to JSON Schema.
1274
  *
1275
  * @return array
1276
  */
1282
  'title' => $this->post_type,
1283
  'type' => 'object',
1284
  /*
1285
+ * Base properties for every Post.
1286
  */
1287
  'properties' => array(
1288
  'date' => array(
1289
+ 'description' => __( "The date the object was published, in the site's timezone." ),
1290
  'type' => 'string',
1291
  'format' => 'date-time',
1292
  'context' => array( 'view', 'edit', 'embed' ),
1293
  ),
1294
  'date_gmt' => array(
1295
+ 'description' => __( 'The date the object was published, as GMT.' ),
1296
  'type' => 'string',
1297
  'format' => 'date-time',
1298
+ 'context' => array( 'view', 'edit' ),
1299
  ),
1300
  'guid' => array(
1301
+ 'description' => __( 'The globally unique identifier for the object.' ),
1302
  'type' => 'object',
1303
  'context' => array( 'view', 'edit' ),
1304
  'readonly' => true,
1305
  'properties' => array(
1306
  'raw' => array(
1307
+ 'description' => __( 'GUID for the object, as it exists in the database.' ),
1308
  'type' => 'string',
1309
  'context' => array( 'edit' ),
1310
  ),
1311
  'rendered' => array(
1312
+ 'description' => __( 'GUID for the object, transformed for display.' ),
1313
  'type' => 'string',
1314
  'context' => array( 'view', 'edit' ),
1315
  ),
1316
  ),
1317
  ),
1318
  'id' => array(
1319
+ 'description' => __( 'Unique identifier for the object.' ),
1320
  'type' => 'integer',
1321
  'context' => array( 'view', 'edit', 'embed' ),
1322
  'readonly' => true,
1323
  ),
1324
  'link' => array(
1325
+ 'description' => __( 'URL to the object.' ),
1326
  'type' => 'string',
1327
  'format' => 'uri',
1328
  'context' => array( 'view', 'edit', 'embed' ),
1329
  'readonly' => true,
1330
  ),
1331
  'modified' => array(
1332
+ 'description' => __( "The date the object was last modified, in the site's timezone." ),
1333
  'type' => 'string',
1334
  'format' => 'date-time',
1335
  'context' => array( 'view', 'edit' ),
1336
+ 'readonly' => true,
1337
  ),
1338
  'modified_gmt' => array(
1339
+ 'description' => __( 'The date the object was last modified, as GMT.' ),
1340
  'type' => 'string',
1341
  'format' => 'date-time',
1342
  'context' => array( 'view', 'edit' ),
1343
+ 'readonly' => true,
1344
  ),
1345
  'password' => array(
1346
+ 'description' => __( 'A password to protect access to the post.' ),
1347
  'type' => 'string',
1348
  'context' => array( 'edit' ),
1349
  ),
1350
  'slug' => array(
1351
+ 'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
1352
  'type' => 'string',
1353
  'context' => array( 'view', 'edit', 'embed' ),
1354
+ 'arg_options' => array(
1355
+ 'sanitize_callback' => 'sanitize_title',
1356
+ ),
1357
  ),
1358
  'status' => array(
1359
+ 'description' => __( 'A named status for the object.' ),
1360
  'type' => 'string',
1361
  'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
1362
  'context' => array( 'edit' ),
1363
  ),
1364
  'type' => array(
1365
+ 'description' => __( 'Type of Post for the object.' ),
1366
  'type' => 'string',
1367
  'context' => array( 'view', 'edit', 'embed' ),
1368
  'readonly' => true,
1373
  $post_type_obj = get_post_type_object( $this->post_type );
1374
  if ( $post_type_obj->hierarchical ) {
1375
  $schema['properties']['parent'] = array(
1376
+ 'description' => __( 'The id for the parent of the object.' ),
1377
  'type' => 'integer',
1378
  'context' => array( 'view', 'edit' ),
1379
  );
1429
 
1430
  case 'title':
1431
  $schema['properties']['title'] = array(
1432
+ 'description' => __( 'The title for the object.' ),
1433
  'type' => 'object',
1434
  'context' => array( 'view', 'edit', 'embed' ),
1435
  'properties' => array(
1436
  'raw' => array(
1437
+ 'description' => __( 'Title for the object, as it exists in the database.' ),
1438
  'type' => 'string',
1439
  'context' => array( 'edit' ),
1440
  ),
1441
  'rendered' => array(
1442
+ 'description' => __( 'Title for the object, transformed for display.' ),
1443
  'type' => 'string',
1444
  'context' => array( 'view', 'edit', 'embed' ),
1445
  ),
1449
 
1450
  case 'editor':
1451
  $schema['properties']['content'] = array(
1452
+ 'description' => __( 'The content for the object.' ),
1453
  'type' => 'object',
1454
  'context' => array( 'view', 'edit' ),
1455
  'properties' => array(
1456
  'raw' => array(
1457
+ 'description' => __( 'Content for the object, as it exists in the database.' ),
1458
  'type' => 'string',
1459
  'context' => array( 'edit' ),
1460
  ),
1461
  'rendered' => array(
1462
+ 'description' => __( 'Content for the object, transformed for display.' ),
1463
  'type' => 'string',
1464
  'context' => array( 'view', 'edit' ),
1465
  ),
1469
 
1470
  case 'author':
1471
  $schema['properties']['author'] = array(
1472
+ 'description' => __( 'The id for the author of the object.' ),
1473
  'type' => 'integer',
1474
  'context' => array( 'view', 'edit', 'embed' ),
1475
  );
1477
 
1478
  case 'excerpt':
1479
  $schema['properties']['excerpt'] = array(
1480
+ 'description' => __( 'The excerpt for the object.' ),
1481
  'type' => 'object',
1482
  'context' => array( 'view', 'edit', 'embed' ),
1483
  'properties' => array(
1484
  'raw' => array(
1485
+ 'description' => __( 'Excerpt for the object, as it exists in the database.' ),
1486
  'type' => 'string',
1487
  'context' => array( 'edit' ),
1488
  ),
1489
  'rendered' => array(
1490
+ 'description' => __( 'Excerpt for the object, transformed for display.' ),
1491
  'type' => 'string',
1492
  'context' => array( 'view', 'edit', 'embed' ),
1493
  ),
1497
 
1498
  case 'thumbnail':
1499
  $schema['properties']['featured_image'] = array(
1500
+ 'description' => __( 'The id of the featured image for the object.' ),
1501
  'type' => 'integer',
1502
  'context' => array( 'view', 'edit' ),
1503
  );
1505
 
1506
  case 'comments':
1507
  $schema['properties']['comment_status'] = array(
1508
+ 'description' => __( 'Whether or not comments are open on the object.' ),
1509
  'type' => 'string',
1510
  'enum' => array( 'open', 'closed' ),
1511
  'context' => array( 'view', 'edit' ),
1512
  );
1513
  $schema['properties']['ping_status'] = array(
1514
+ 'description' => __( 'Whether or not the object can be pinged.' ),
1515
  'type' => 'string',
1516
  'enum' => array( 'open', 'closed' ),
1517
  'context' => array( 'view', 'edit' ),
1520
 
1521
  case 'page-attributes':
1522
  $schema['properties']['menu_order'] = array(
1523
+ 'description' => __( 'The order of the object in relation to other object of its type.' ),
1524
  'type' => 'integer',
1525
  'context' => array( 'view', 'edit' ),
1526
  );
1528
 
1529
  case 'post-formats':
1530
  $schema['properties']['format'] = array(
1531
+ 'description' => __( 'The format for the object.' ),
1532
  'type' => 'string',
1533
+ 'enum' => array_values( get_post_format_slugs() ),
1534
  'context' => array( 'view', 'edit' ),
1535
  );
1536
  break;
1540
 
1541
  if ( 'post' === $this->post_type ) {
1542
  $schema['properties']['sticky'] = array(
1543
+ 'description' => __( 'Whether or not the object should be treated as sticky.' ),
1544
  'type' => 'boolean',
1545
  'context' => array( 'view', 'edit' ),
1546
  );
1548
 
1549
  if ( 'page' === $this->post_type ) {
1550
  $schema['properties']['template'] = array(
1551
+ 'description' => __( 'The theme file to use to display the object.' ),
1552
  'type' => 'string',
1553
+ 'enum' => array_keys( wp_get_theme()->get_page_templates() ),
1554
  'context' => array( 'view', 'edit' ),
1555
  );
1556
  }
1558
  return $this->add_additional_fields_schema( $schema );
1559
  }
1560
 
1561
+ /**
1562
+ * Get the query params for collections of attachments.
1563
+ *
1564
+ * @return array
1565
+ */
1566
+ public function get_collection_params() {
1567
+ $params = parent::get_collection_params();
1568
+
1569
+ $params['context']['default'] = 'view';
1570
+
1571
+ if ( post_type_supports( $this->post_type, 'author' ) ) {
1572
+ $params['author'] = array(
1573
+ 'description' => __( 'Limit result set to posts assigned to a specific author.' ),
1574
+ 'type' => 'integer',
1575
+ 'default' => null,
1576
+ 'sanitize_callback' => 'absint',
1577
+ );
1578
+ }
1579
+ $params['include'] = array(
1580
+ 'description' => __( 'Limit result set to specific ids.' ),
1581
+ 'type' => 'array',
1582
+ 'default' => array(),
1583
+ 'sanitize_callback' => 'wp_parse_id_list',
1584
+ );
1585
+ $params['order'] = array(
1586
+ 'description' => __( 'Order sort attribute ascending or descending.' ),
1587
+ 'type' => 'string',
1588
+ 'default' => 'desc',
1589
+ 'enum' => array( 'asc', 'desc' ),
1590
+ );
1591
+ $params['orderby'] = array(
1592
+ 'description' => __( 'Sort collection by object attribute.' ),
1593
+ 'type' => 'string',
1594
+ 'default' => 'date',
1595
+ 'enum' => array(
1596
+ 'date',
1597
+ 'id',
1598
+ 'include',
1599
+ 'title',
1600
+ 'slug',
1601
+ ),
1602
+ );
1603
+
1604
+ $post_type_obj = get_post_type_object( $this->post_type );
1605
+ if ( $post_type_obj->hierarchical ) {
1606
+ $params['parent'] = array(
1607
+ 'description' => _( 'Limit result set to that of a specific parent id.' ),
1608
+ 'type' => 'integer',
1609
+ 'sanitize_callback' => 'absint',
1610
+ 'default' => null,
1611
+ );
1612
+ }
1613
+
1614
+ $params['status'] = array(
1615
+ 'default' => 'attachment' === $this->post_type ? 'inherit' : 'publish',
1616
+ 'description' => __( 'Limit result set to posts assigned a specific status.' ),
1617
+ 'sanitize_callback' => 'sanitize_key',
1618
+ 'type' => 'string',
1619
+ 'validate_callback' => array( $this, 'validate_user_can_query_private_statuses' ),
1620
+ );
1621
+ $params['filter'] = array(
1622
+ 'description' => __( 'Use WP Query arguments to modify the response; private query vars require appropriate authorization.' ),
1623
+ );
1624
+ return $params;
1625
+ }
1626
+
1627
+ /**
1628
+ * Validate whether the user can query private statuses
1629
+ *
1630
+ * @param mixed $value
1631
+ * @param WP_REST_Request $request
1632
+ * @param string $parameter
1633
+ * @return WP_Error|bool
1634
+ */
1635
+ public function validate_user_can_query_private_statuses( $value, $request, $parameter ) {
1636
+ if ( 'publish' === $value || ( 'attachment' === $this->post_type && 'inherit' === $value ) ) {
1637
+ return true;
1638
+ }
1639
+ $post_type_obj = get_post_type_object( $this->post_type );
1640
+ if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
1641
+ return true;
1642
+ }
1643
+ return new WP_Error( 'rest_forbidden_status', __( 'Status is forbidden' ), array( 'status' => rest_authorization_required_code() ) );
1644
+ }
1645
+
1646
  }
lib/endpoints/class-wp-rest-posts-terms-controller.php CHANGED
@@ -16,39 +16,44 @@ class WP_REST_Posts_Terms_Controller extends WP_REST_Controller {
16
  */
17
  public function register_routes() {
18
 
19
- $base = $this->posts_controller->get_post_type_base( $this->post_type );
 
20
 
21
- $query_params = $this->get_collection_params();
22
- register_rest_route( 'wp/v2', sprintf( '/%s/(?P<post_id>[\d]+)/terms/%s', $base, $this->taxonomy ), array(
23
  array(
24
  'methods' => WP_REST_Server::READABLE,
25
  'callback' => array( $this, 'get_items' ),
26
  'permission_callback' => array( $this, 'get_items_permissions_check' ),
27
- 'args' => $query_params,
28
  ),
 
29
  ) );
30
 
31
- register_rest_route( 'wp/v2', sprintf( '/%s/(?P<post_id>[\d]+)/terms/%s/(?P<term_id>[\d]+)', $base, $this->taxonomy ), array(
32
  array(
33
  'methods' => WP_REST_Server::READABLE,
34
  'callback' => array( $this, 'get_item' ),
35
  'permission_callback' => array( $this, 'get_items_permissions_check' ),
 
 
 
36
  ),
37
  array(
38
  'methods' => WP_REST_Server::CREATABLE,
39
  'callback' => array( $this, 'create_item' ),
40
- 'permission_callback' => array( $this, 'create_item_permissions_check' ),
41
  ),
42
  array(
43
  'methods' => WP_REST_Server::DELETABLE,
44
  'callback' => array( $this, 'delete_item' ),
45
- 'permission_callback' => array( $this, 'create_item_permissions_check' ),
 
 
 
 
 
46
  ),
47
- ) );
48
-
49
- register_rest_route( 'wp/v2', sprintf( '/%s/(?P<post_id>[\d]+)/terms/%s', $base, $this->taxonomy ) . '/schema', array(
50
- 'methods' => WP_REST_Server::READABLE,
51
- 'callback' => array( $this, 'get_public_item_schema' ),
52
  ) );
53
  }
54
 
@@ -101,11 +106,11 @@ class WP_REST_Posts_Terms_Controller extends WP_REST_Controller {
101
 
102
  $terms = wp_get_object_terms( $post->ID, $this->taxonomy );
103
 
104
- if ( ! in_array( $term_id, wp_list_pluck( $terms, 'term_taxonomy_id' ) ) ) {
105
- return new WP_Error( 'rest_post_not_in_term', __( 'Invalid taxonomy for post ID.' ), array( 'status' => 404 ) );
106
  }
107
 
108
- $term = $this->terms_controller->prepare_item_for_response( get_term_by( 'term_taxonomy_id', $term_id, $this->taxonomy ), $request );
109
 
110
  $response = rest_ensure_response( $term );
111
 
@@ -127,17 +132,27 @@ class WP_REST_Posts_Terms_Controller extends WP_REST_Controller {
127
  return $is_request_valid;
128
  }
129
 
130
- $tt_ids = wp_set_object_terms( $post->ID, $term_id, $this->taxonomy, true );
 
131
 
132
  if ( is_wp_error( $tt_ids ) ) {
133
  return $tt_ids;
134
  }
135
 
136
- $term = $this->terms_controller->prepare_item_for_response( get_term_by( 'term_taxonomy_id', $term_id, $this->taxonomy ), $request );
137
 
138
  $response = rest_ensure_response( $term );
139
  $response->set_status( 201 );
140
 
 
 
 
 
 
 
 
 
 
141
  return $term;
142
  }
143
 
@@ -170,6 +185,15 @@ class WP_REST_Posts_Terms_Controller extends WP_REST_Controller {
170
  return $remove;
171
  }
172
 
 
 
 
 
 
 
 
 
 
173
  return $previous_item;
174
  }
175
 
@@ -185,23 +209,25 @@ class WP_REST_Posts_Terms_Controller extends WP_REST_Controller {
185
  /**
186
  * Validate the API request for relationship requests.
187
  *
188
- * @param WP_REST_Request $request
189
  * @return WP_Error|true
190
  */
191
  protected function validate_request( $request ) {
 
192
 
193
- $post_request = new WP_REST_Request();
194
- $post_request->set_param( 'id', $request['post_id'] );
 
195
 
196
- $post_check = $this->posts_controller->get_item( $post_request );
197
- if ( is_wp_error( $post_check ) ) {
198
- return $post_check;
199
  }
200
 
201
  if ( ! empty( $request['term_id'] ) ) {
202
  $term_id = absint( $request['term_id'] );
203
 
204
- if ( ! get_term_by( 'term_taxonomy_id', $term_id, $this->taxonomy ) ) {
 
205
  return new WP_Error( 'rest_term_invalid', __( "Term doesn't exist." ), array( 'status' => 404 ) );
206
  }
207
  }
@@ -239,12 +265,17 @@ class WP_REST_Posts_Terms_Controller extends WP_REST_Controller {
239
  }
240
 
241
  /**
242
- * Check if a given request has access to create a post/term relationship.
243
  *
244
  * @param WP_REST_Request $request Full details about the request.
245
  * @return bool|WP_Error
246
  */
247
- public function create_item_permissions_check( $request ) {
 
 
 
 
 
248
 
249
  $post_request = new WP_REST_Request();
250
  $post_request->set_param( 'id', $request['post_id'] );
@@ -264,14 +295,15 @@ class WP_REST_Posts_Terms_Controller extends WP_REST_Controller {
264
  */
265
  public function get_collection_params() {
266
  $query_params = array();
 
267
  $query_params['order'] = array(
268
- 'description' => 'Order sort attribute ascending or descending.',
269
  'type' => 'string',
270
  'default' => 'asc',
271
  'enum' => array( 'asc', 'desc' ),
272
  );
273
  $query_params['orderby'] = array(
274
- 'description' => 'Sort collection by object attribute.',
275
  'type' => 'string',
276
  'default' => 'name',
277
  'enum' => array(
16
  */
17
  public function register_routes() {
18
 
19
+ $base = $this->posts_controller->get_post_type_base( $this->post_type );
20
+ $tax_base = $this->terms_controller->get_taxonomy_base( $this->taxonomy );
21
 
22
+ register_rest_route( 'wp/v2', sprintf( '/%s/(?P<post_id>[\d]+)/%s', $base, $tax_base ), array(
 
23
  array(
24
  'methods' => WP_REST_Server::READABLE,
25
  'callback' => array( $this, 'get_items' ),
26
  'permission_callback' => array( $this, 'get_items_permissions_check' ),
27
+ 'args' => $this->get_collection_params(),
28
  ),
29
+ 'schema' => array( $this, 'get_public_item_schema' ),
30
  ) );
31
 
32
+ register_rest_route( 'wp/v2', sprintf( '/%s/(?P<post_id>[\d]+)/%s/(?P<term_id>[\d]+)', $base, $tax_base ), array(
33
  array(
34
  'methods' => WP_REST_Server::READABLE,
35
  'callback' => array( $this, 'get_item' ),
36
  'permission_callback' => array( $this, 'get_items_permissions_check' ),
37
+ 'args' => array(
38
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
39
+ ),
40
  ),
41
  array(
42
  'methods' => WP_REST_Server::CREATABLE,
43
  'callback' => array( $this, 'create_item' ),
44
+ 'permission_callback' => array( $this, 'manage_item_permissions_check' ),
45
  ),
46
  array(
47
  'methods' => WP_REST_Server::DELETABLE,
48
  'callback' => array( $this, 'delete_item' ),
49
+ 'permission_callback' => array( $this, 'manage_item_permissions_check' ),
50
+ 'args' => array(
51
+ 'force' => array(
52
+ 'default' => false,
53
+ ),
54
+ ),
55
  ),
56
+ 'schema' => array( $this, 'get_public_item_schema' ),
 
 
 
 
57
  ) );
58
  }
59
 
106
 
107
  $terms = wp_get_object_terms( $post->ID, $this->taxonomy );
108
 
109
+ if ( ! in_array( $term_id, wp_list_pluck( $terms, 'term_id' ) ) ) {
110
+ return new WP_Error( 'rest_post_not_in_term', __( 'Invalid taxonomy for post id.' ), array( 'status' => 404 ) );
111
  }
112
 
113
+ $term = $this->terms_controller->prepare_item_for_response( get_term( $term_id, $this->taxonomy ), $request );
114
 
115
  $response = rest_ensure_response( $term );
116
 
132
  return $is_request_valid;
133
  }
134
 
135
+ $term = get_term( $term_id, $this->taxonomy );
136
+ $tt_ids = wp_set_object_terms( $post->ID, $term->term_id, $this->taxonomy, true );
137
 
138
  if ( is_wp_error( $tt_ids ) ) {
139
  return $tt_ids;
140
  }
141
 
142
+ $term = $this->terms_controller->prepare_item_for_response( get_term( $term_id, $this->taxonomy ), $request );
143
 
144
  $response = rest_ensure_response( $term );
145
  $response->set_status( 201 );
146
 
147
+ /**
148
+ * Fires after a term is added to a post via the REST API.
149
+ *
150
+ * @param array $term The added term data.
151
+ * @param WP_Post $post The post the term was added to.
152
+ * @param WP_REST_Request $request The request sent to the API.
153
+ */
154
+ do_action( 'rest_insert_term', $term, $post, $request );
155
+
156
  return $term;
157
  }
158
 
185
  return $remove;
186
  }
187
 
188
+ /**
189
+ * Fires after a term is removed from a post via the REST API.
190
+ *
191
+ * @param array $previous_item The removed term data.
192
+ * @param WP_Post $post The post the term was removed from.
193
+ * @param WP_REST_Request $request The request sent to the API.
194
+ */
195
+ do_action( 'rest_remove_term', $previous_item, $post, $request );
196
+
197
  return $previous_item;
198
  }
199
 
209
  /**
210
  * Validate the API request for relationship requests.
211
  *
212
+ * @param WP_REST_Request $request Full data about the request.
213
  * @return WP_Error|true
214
  */
215
  protected function validate_request( $request ) {
216
+ $post = get_post( (int) $request['post_id'] );
217
 
218
+ if ( empty( $post ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) {
219
+ return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
220
+ }
221
 
222
+ if ( ! $this->posts_controller->check_read_permission( $post ) ) {
223
+ return new WP_Error( 'rest_forbidden', __( 'Sorry, you cannot view this post.' ), array( 'status' => rest_authorization_required_code() ) );
 
224
  }
225
 
226
  if ( ! empty( $request['term_id'] ) ) {
227
  $term_id = absint( $request['term_id'] );
228
 
229
+ $term = get_term( $term_id, $this->taxonomy );
230
+ if ( ! $term || $term->taxonomy !== $this->taxonomy ) {
231
  return new WP_Error( 'rest_term_invalid', __( "Term doesn't exist." ), array( 'status' => 404 ) );
232
  }
233
  }
265
  }
266
 
267
  /**
268
+ * Check if a given request has access to manage a post/term relationship.
269
  *
270
  * @param WP_REST_Request $request Full details about the request.
271
  * @return bool|WP_Error
272
  */
273
+ public function manage_item_permissions_check( $request ) {
274
+
275
+ $taxonomy_obj = get_taxonomy( $this->taxonomy );
276
+ if ( ! current_user_can( $taxonomy_obj->cap->assign_terms ) ) {
277
+ return new WP_Error( 'rest_cannot_assign', __( 'Sorry, you are not allowed to assign terms.' ), array( 'status' => rest_authorization_required_code() ) );
278
+ }
279
 
280
  $post_request = new WP_REST_Request();
281
  $post_request->set_param( 'id', $request['post_id'] );
295
  */
296
  public function get_collection_params() {
297
  $query_params = array();
298
+ $query_params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
299
  $query_params['order'] = array(
300
+ 'description' => __( 'Order sort attribute ascending or descending.' ),
301
  'type' => 'string',
302
  'default' => 'asc',
303
  'enum' => array( 'asc', 'desc' ),
304
  );
305
  $query_params['orderby'] = array(
306
+ 'description' => __( 'Sort collection by object attribute.' ),
307
  'type' => 'string',
308
  'default' => 'name',
309
  'enum' => array(
lib/endpoints/class-wp-rest-revisions-controller.php CHANGED
@@ -18,14 +18,14 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
18
  public function register_routes() {
19
 
20
  register_rest_route( 'wp/v2', '/' . $this->parent_base . '/(?P<parent_id>[\d]+)/revisions', array(
21
- 'methods' => WP_REST_Server::READABLE,
22
- 'callback' => array( $this, 'get_items' ),
23
- 'permission_callback' => array( $this, 'get_items_permissions_check' ),
24
- 'args' => array(
25
- 'context' => array(
26
- 'default' => 'view',
27
- ),
28
  ),
 
 
29
  ) );
30
 
31
  register_rest_route( 'wp/v2', '/' . $this->parent_base . '/(?P<parent_id>[\d]+)/revisions/(?P<id>[\d]+)', array(
@@ -34,9 +34,7 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
34
  'callback' => array( $this, 'get_item' ),
35
  'permission_callback' => array( $this, 'get_item_permissions_check' ),
36
  'args' => array(
37
- 'context' => array(
38
- 'default' => 'view',
39
- ),
40
  ),
41
  ),
42
  array(
@@ -44,12 +42,9 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
44
  'callback' => array( $this, 'delete_item' ),
45
  'permission_callback' => array( $this, 'delete_item_permissions_check' ),
46
  ),
47
- ));
48
 
49
- register_rest_route( 'wp/v2', '/' . $this->parent_base . '/revisions/schema', array(
50
- 'methods' => WP_REST_Server::READABLE,
51
- 'callback' => array( $this, 'get_public_item_schema' ),
52
- ) );
53
 
54
  }
55
 
@@ -63,16 +58,17 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
63
 
64
  $parent = get_post( $request['parent_id'] );
65
  if ( ! $request['parent_id'] || ! $parent || $this->parent_post_type !== $parent->post_type ) {
66
- return new WP_Error( 'rest_post_invalid_parent_id', __( 'Invalid post parent ID.' ), array( 'status' => 404 ) );
67
  }
68
 
69
  $revisions = wp_get_post_revisions( $request['parent_id'] );
70
 
71
- $struct = array();
72
  foreach ( $revisions as $revision ) {
73
- $struct[] = $this->prepare_item_for_response( $revision, $request );
 
74
  }
75
- return $struct;
76
  }
77
 
78
  /**
@@ -89,7 +85,7 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
89
  }
90
  $parent_post_type_obj = get_post_type_object( $parent->post_type );
91
  if ( ! current_user_can( $parent_post_type_obj->cap->edit_post, $parent->ID ) ) {
92
- return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot view revisions of this post.' ), array( 'status' => 403 ) );
93
  }
94
 
95
  return true;
@@ -105,12 +101,12 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
105
 
106
  $parent = get_post( $request['parent_id'] );
107
  if ( ! $request['parent_id'] || ! $parent || $this->parent_post_type !== $parent->post_type ) {
108
- return new WP_Error( 'rest_post_invalid_parent_id', __( 'Invalid post parent ID.' ), array( 'status' => 404 ) );
109
  }
110
 
111
  $revision = get_post( $request['id'] );
112
  if ( ! $revision || 'revision' !== $revision->post_type ) {
113
- return new WP_Error( 'rest_post_invalid_id', __( 'Invalid revision ID.' ), array( 'status' => 404 ) );
114
  }
115
 
116
  $response = $this->prepare_item_for_response( $revision, $request );
@@ -135,6 +131,17 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
135
  */
136
  public function delete_item( $request ) {
137
  $result = wp_delete_post( $request['id'], true );
 
 
 
 
 
 
 
 
 
 
 
138
  if ( $result ) {
139
  return true;
140
  } else {
@@ -163,7 +170,7 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
163
  /**
164
  * Prepare the revision for the REST response
165
  *
166
- * @param mixed $item WordPress representation of the revision.
167
  * @param WP_REST_Request $request Request object.
168
  * @return array
169
  */
@@ -208,7 +215,16 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
208
  $response->add_link( 'parent', rest_url( sprintf( 'wp/%s/%d', $this->parent_base, $data['parent'] ) ) );
209
  }
210
 
211
- return $response;
 
 
 
 
 
 
 
 
 
212
  }
213
 
214
  /**
@@ -225,10 +241,10 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
225
  }
226
 
227
  if ( isset( $date ) ) {
228
- return rest_mysql_to_rfc3339( $date );
229
  }
230
 
231
- return rest_mysql_to_rfc3339( $date_gmt );
232
  }
233
 
234
  /**
@@ -246,51 +262,51 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
246
  */
247
  'properties' => array(
248
  'author' => array(
249
- 'description' => 'The ID for the author of the object.',
250
  'type' => 'integer',
251
  'context' => array( 'view' ),
252
  ),
253
  'date' => array(
254
- 'description' => 'The date the object was published.',
255
  'type' => 'string',
256
  'format' => 'date-time',
257
  'context' => array( 'view' ),
258
  ),
259
  'date_gmt' => array(
260
- 'description' => 'The date the object was published, as GMT.',
261
  'type' => 'string',
262
  'format' => 'date-time',
263
  'context' => array( 'view' ),
264
  ),
265
  'guid' => array(
266
- 'description' => 'GUID for the object, as it exists in the database.',
267
  'type' => 'string',
268
  'context' => array( 'view' ),
269
  ),
270
  'id' => array(
271
- 'description' => 'Unique identifier for the object.',
272
  'type' => 'integer',
273
  'context' => array( 'view' ),
274
  ),
275
  'modified' => array(
276
- 'description' => 'The date the object was last modified.',
277
  'type' => 'string',
278
  'format' => 'date-time',
279
  'context' => array( 'view' ),
280
  ),
281
  'modified_gmt' => array(
282
- 'description' => 'The date the object was last modified, as GMT.',
283
  'type' => 'string',
284
  'format' => 'date-time',
285
  'context' => array( 'view' ),
286
  ),
287
  'parent' => array(
288
- 'description' => 'The ID for the parent of the object.',
289
  'type' => 'integer',
290
  'context' => array( 'view' ),
291
  ),
292
  'slug' => array(
293
- 'description' => 'An alphanumeric identifier for the object unique to its type.',
294
  'type' => 'string',
295
  'context' => array( 'view' ),
296
  ),
@@ -308,7 +324,7 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
308
 
309
  case 'title':
310
  $schema['properties']['title'] = array(
311
- 'description' => 'Title for the object, as it exists in the database.',
312
  'type' => 'string',
313
  'context' => array( 'view' ),
314
  );
@@ -316,7 +332,7 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
316
 
317
  case 'content':
318
  $schema['properties']['content'] = array(
319
- 'description' => 'Content for the object, as it exists in the database.',
320
  'type' => 'string',
321
  'context' => array( 'view' ),
322
  );
@@ -324,7 +340,7 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
324
 
325
  case 'excerpt':
326
  $schema['properties']['excerpt'] = array(
327
- 'description' => 'Excerpt for the object, as it exists in the database.',
328
  'type' => 'string',
329
  'context' => array( 'view' ),
330
  );
@@ -336,4 +352,15 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
336
  return $this->add_additional_fields_schema( $schema );
337
  }
338
 
 
 
 
 
 
 
 
 
 
 
 
339
  }
18
  public function register_routes() {
19
 
20
  register_rest_route( 'wp/v2', '/' . $this->parent_base . '/(?P<parent_id>[\d]+)/revisions', array(
21
+ array(
22
+ 'methods' => WP_REST_Server::READABLE,
23
+ 'callback' => array( $this, 'get_items' ),
24
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
25
+ 'args' => $this->get_collection_params(),
 
 
26
  ),
27
+
28
+ 'schema' => array( $this, 'get_public_item_schema' ),
29
  ) );
30
 
31
  register_rest_route( 'wp/v2', '/' . $this->parent_base . '/(?P<parent_id>[\d]+)/revisions/(?P<id>[\d]+)', array(
34
  'callback' => array( $this, 'get_item' ),
35
  'permission_callback' => array( $this, 'get_item_permissions_check' ),
36
  'args' => array(
37
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
 
 
38
  ),
39
  ),
40
  array(
42
  'callback' => array( $this, 'delete_item' ),
43
  'permission_callback' => array( $this, 'delete_item_permissions_check' ),
44
  ),
 
45
 
46
+ 'schema' => array( $this, 'get_public_item_schema' ),
47
+ ));
 
 
48
 
49
  }
50
 
58
 
59
  $parent = get_post( $request['parent_id'] );
60
  if ( ! $request['parent_id'] || ! $parent || $this->parent_post_type !== $parent->post_type ) {
61
+ return new WP_Error( 'rest_post_invalid_parent_id', __( 'Invalid post parent id.' ), array( 'status' => 404 ) );
62
  }
63
 
64
  $revisions = wp_get_post_revisions( $request['parent_id'] );
65
 
66
+ $response = array();
67
  foreach ( $revisions as $revision ) {
68
+ $data = $this->prepare_item_for_response( $revision, $request );
69
+ $response[] = $this->prepare_response_for_collection( $data );
70
  }
71
+ return $response;
72
  }
73
 
74
  /**
85
  }
86
  $parent_post_type_obj = get_post_type_object( $parent->post_type );
87
  if ( ! current_user_can( $parent_post_type_obj->cap->edit_post, $parent->ID ) ) {
88
+ return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot view revisions of this post.' ), array( 'status' => rest_authorization_required_code() ) );
89
  }
90
 
91
  return true;
101
 
102
  $parent = get_post( $request['parent_id'] );
103
  if ( ! $request['parent_id'] || ! $parent || $this->parent_post_type !== $parent->post_type ) {
104
+ return new WP_Error( 'rest_post_invalid_parent_id', __( 'Invalid post parent id.' ), array( 'status' => 404 ) );
105
  }
106
 
107
  $revision = get_post( $request['id'] );
108
  if ( ! $revision || 'revision' !== $revision->post_type ) {
109
+ return new WP_Error( 'rest_post_invalid_id', __( 'Invalid revision id.' ), array( 'status' => 404 ) );
110
  }
111
 
112
  $response = $this->prepare_item_for_response( $revision, $request );
131
  */
132
  public function delete_item( $request ) {
133
  $result = wp_delete_post( $request['id'], true );
134
+
135
+ /**
136
+ * Fires after a revision is deleted via the REST API.
137
+ *
138
+ * @param (mixed) $result The revision object (if it was deleted or moved to the trash successfully)
139
+ * or false (failure). If the revision was moved to to the trash, $result represents
140
+ * its new state; if it was deleted, $result represents its state before deletion.
141
+ * @param WP_REST_Request $request The request sent to the API.
142
+ */
143
+ do_action( 'rest_delete_revision', $result, $request );
144
+
145
  if ( $result ) {
146
  return true;
147
  } else {
170
  /**
171
  * Prepare the revision for the REST response
172
  *
173
+ * @param WP_Post $post Post revision object.
174
  * @param WP_REST_Request $request Request object.
175
  * @return array
176
  */
215
  $response->add_link( 'parent', rest_url( sprintf( 'wp/%s/%d', $this->parent_base, $data['parent'] ) ) );
216
  }
217
 
218
+ /**
219
+ * Filter a revision returned from the API.
220
+ *
221
+ * Allows modification of the revision right before it is returned.
222
+ *
223
+ * @param WP_REST_Response $response The response object.
224
+ * @param WP_Post $post The original revision object.
225
+ * @param WP_REST_Request $request Request used to generate the response.
226
+ */
227
+ return apply_filters( 'rest_prepare_revision', $response, $post, $request );
228
  }
229
 
230
  /**
241
  }
242
 
243
  if ( isset( $date ) ) {
244
+ return mysql_to_rfc3339( $date );
245
  }
246
 
247
+ return mysql_to_rfc3339( $date_gmt );
248
  }
249
 
250
  /**
262
  */
263
  'properties' => array(
264
  'author' => array(
265
+ 'description' => __( 'The id for the author of the object.' ),
266
  'type' => 'integer',
267
  'context' => array( 'view' ),
268
  ),
269
  'date' => array(
270
+ 'description' => __( 'The date the object was published.' ),
271
  'type' => 'string',
272
  'format' => 'date-time',
273
  'context' => array( 'view' ),
274
  ),
275
  'date_gmt' => array(
276
+ 'description' => __( 'The date the object was published, as GMT.' ),
277
  'type' => 'string',
278
  'format' => 'date-time',
279
  'context' => array( 'view' ),
280
  ),
281
  'guid' => array(
282
+ 'description' => __( 'GUID for the object, as it exists in the database.' ),
283
  'type' => 'string',
284
  'context' => array( 'view' ),
285
  ),
286
  'id' => array(
287
+ 'description' => __( 'Unique identifier for the object.' ),
288
  'type' => 'integer',
289
  'context' => array( 'view' ),
290
  ),
291
  'modified' => array(
292
+ 'description' => __( 'The date the object was last modified.' ),
293
  'type' => 'string',
294
  'format' => 'date-time',
295
  'context' => array( 'view' ),
296
  ),
297
  'modified_gmt' => array(
298
+ 'description' => __( 'The date the object was last modified, as GMT.' ),
299
  'type' => 'string',
300
  'format' => 'date-time',
301
  'context' => array( 'view' ),
302
  ),
303
  'parent' => array(
304
+ 'description' => __( 'The id for the parent of the object.' ),
305
  'type' => 'integer',
306
  'context' => array( 'view' ),
307
  ),
308
  'slug' => array(
309
+ 'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
310
  'type' => 'string',
311
  'context' => array( 'view' ),
312
  ),
324
 
325
  case 'title':
326
  $schema['properties']['title'] = array(
327
+ 'description' => __( 'Title for the object, as it exists in the database.' ),
328
  'type' => 'string',
329
  'context' => array( 'view' ),
330
  );
332
 
333
  case 'content':
334
  $schema['properties']['content'] = array(
335
+ 'description' => __( 'Content for the object, as it exists in the database.' ),
336
  'type' => 'string',
337
  'context' => array( 'view' ),
338
  );
340
 
341
  case 'excerpt':
342
  $schema['properties']['excerpt'] = array(
343
+ 'description' => __( 'Excerpt for the object, as it exists in the database.' ),
344
  'type' => 'string',
345
  'context' => array( 'view' ),
346
  );
352
  return $this->add_additional_fields_schema( $schema );
353
  }
354
 
355
+ /**
356
+ * Get the query params for collections
357
+ *
358
+ * @return array
359
+ */
360
+ public function get_collection_params() {
361
+ return array(
362
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
363
+ );
364
+ }
365
+
366
  }
lib/endpoints/class-wp-rest-taxonomies-controller.php CHANGED
@@ -8,22 +8,24 @@ class WP_REST_Taxonomies_Controller extends WP_REST_Controller {
8
  public function register_routes() {
9
 
10
  register_rest_route( 'wp/v2', '/taxonomies', array(
11
- 'methods' => WP_REST_Server::READABLE,
12
- 'callback' => array( $this, 'get_items' ),
13
- 'args' => array(
14
- 'post_type' => array(
15
- 'sanitize_callback' => 'sanitize_key',
16
- ),
17
  ),
 
18
  ) );
19
- register_rest_route( 'wp/v2', '/taxonomies/schema', array(
20
- 'methods' => WP_REST_Server::READABLE,
21
- 'callback' => array( $this, 'get_public_item_schema' ),
22
- ) );
23
  register_rest_route( 'wp/v2', '/taxonomies/(?P<taxonomy>[\w-]+)', array(
24
- 'methods' => WP_REST_Server::READABLE,
25
- 'callback' => array( $this, 'get_item' ),
26
- 'permission_callback' => array( $this, 'get_item_permissions_check' ),
 
 
 
 
 
 
27
  ) );
28
  }
29
 
@@ -34,18 +36,19 @@ class WP_REST_Taxonomies_Controller extends WP_REST_Controller {
34
  * @return array
35
  */
36
  public function get_items( $request ) {
37
- if ( ! empty( $request['post_type'] ) ) {
38
- $taxonomies = get_object_taxonomies( $request['post_type'], 'objects' );
39
  } else {
40
  $taxonomies = get_taxonomies( '', 'objects' );
41
  }
42
  $data = array();
43
  foreach ( $taxonomies as $tax_type => $value ) {
44
- $tax = $this->prepare_item_for_response( $value, $request );
45
- if ( is_wp_error( $tax ) ) {
46
  continue;
47
  }
48
- $data[] = $tax;
 
 
49
  }
50
  return $data;
51
  }
@@ -74,8 +77,13 @@ class WP_REST_Taxonomies_Controller extends WP_REST_Controller {
74
 
75
  $tax_obj = get_taxonomy( $request['taxonomy'] );
76
 
77
- if ( $tax_obj && empty( $tax_obj->show_in_rest ) ) {
78
- return false;
 
 
 
 
 
79
  }
80
 
81
  return true;
@@ -89,9 +97,6 @@ class WP_REST_Taxonomies_Controller extends WP_REST_Controller {
89
  * @return array Taxonomy data
90
  */
91
  public function prepare_item_for_response( $taxonomy, $request ) {
92
- if ( empty( $taxonomy->show_in_rest ) ) {
93
- return new WP_Error( 'rest_cannot_read_taxonomy', __( 'Cannot view taxonomy' ), array( 'status' => 403 ) );
94
- }
95
 
96
  $data = array(
97
  'name' => $taxonomy->label,
@@ -107,7 +112,29 @@ class WP_REST_Taxonomies_Controller extends WP_REST_Controller {
107
  $data = $this->filter_response_by_context( $data, $context );
108
  $data = $this->add_additional_fields_to_object( $data, $request );
109
 
110
- return apply_filters( 'rest_prepare_taxonomy', $data, $taxonomy, $request );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  }
112
 
113
  /**
@@ -122,43 +149,58 @@ class WP_REST_Taxonomies_Controller extends WP_REST_Controller {
122
  'type' => 'object',
123
  'properties' => array(
124
  'description' => array(
125
- 'description' => 'A human-readable description of the object.',
126
  'type' => 'string',
127
- 'context' => array( 'view' ),
128
  ),
129
  'hierarchical' => array(
130
- 'description' => 'Whether or not the type should have children.',
131
  'type' => 'boolean',
132
- 'context' => array( 'view' ),
133
  ),
134
  'labels' => array(
135
- 'description' => 'Human-readable labels for the type for various contexts.',
136
  'type' => 'object',
137
- 'context' => array( 'view' ),
138
  ),
139
  'name' => array(
140
- 'description' => 'The title for the object.',
141
  'type' => 'string',
142
- 'context' => array( 'view' ),
143
  ),
144
  'slug' => array(
145
- 'description' => 'An alphanumeric identifier for the object.',
146
  'type' => 'string',
147
- 'context' => array( 'view' ),
148
  ),
149
  'show_cloud' => array(
150
- 'description' => 'Whether or not the term cloud should be displayed.',
151
  'type' => 'boolean',
152
- 'context' => array( 'view' ),
153
  ),
154
  'types' => array(
155
- 'description' => 'Types associated with taxonomy.',
156
  'type' => 'array',
157
- 'context' => array( 'view' ),
158
  ),
159
  ),
160
  );
161
  return $this->add_additional_fields_schema( $schema );
162
  }
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  }
8
  public function register_routes() {
9
 
10
  register_rest_route( 'wp/v2', '/taxonomies', array(
11
+ array(
12
+ 'methods' => WP_REST_Server::READABLE,
13
+ 'callback' => array( $this, 'get_items' ),
14
+ 'args' => $this->get_collection_params(),
 
 
15
  ),
16
+ 'schema' => array( $this, 'get_public_item_schema' ),
17
  ) );
18
+
 
 
 
19
  register_rest_route( 'wp/v2', '/taxonomies/(?P<taxonomy>[\w-]+)', array(
20
+ array(
21
+ 'methods' => WP_REST_Server::READABLE,
22
+ 'callback' => array( $this, 'get_item' ),
23
+ 'permission_callback' => array( $this, 'get_item_permissions_check' ),
24
+ 'args' => array(
25
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
26
+ ),
27
+ ),
28
+ 'schema' => array( $this, 'get_public_item_schema' ),
29
  ) );
30
  }
31
 
36
  * @return array
37
  */
38
  public function get_items( $request ) {
39
+ if ( ! empty( $request['type'] ) ) {
40
+ $taxonomies = get_object_taxonomies( $request['type'], 'objects' );
41
  } else {
42
  $taxonomies = get_taxonomies( '', 'objects' );
43
  }
44
  $data = array();
45
  foreach ( $taxonomies as $tax_type => $value ) {
46
+ if ( empty( $value->show_in_rest ) || ( 'edit' === $request['context'] && ! current_user_can( $value->cap->manage_terms ) ) ) {
 
47
  continue;
48
  }
49
+ $tax = $this->prepare_item_for_response( $value, $request );
50
+ $tax = $this->prepare_response_for_collection( $tax );
51
+ $data[ $tax_type ] = $tax;
52
  }
53
  return $data;
54
  }
77
 
78
  $tax_obj = get_taxonomy( $request['taxonomy'] );
79
 
80
+ if ( $tax_obj ) {
81
+ if ( empty( $tax_obj->show_in_rest ) ) {
82
+ return false;
83
+ }
84
+ if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->manage_terms ) ) {
85
+ return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to manage this taxonomy.' ), array( 'status' => rest_authorization_required_code() ) );
86
+ }
87
  }
88
 
89
  return true;
97
  * @return array Taxonomy data
98
  */
99
  public function prepare_item_for_response( $taxonomy, $request ) {
 
 
 
100
 
101
  $data = array(
102
  'name' => $taxonomy->label,
112
  $data = $this->filter_response_by_context( $data, $context );
113
  $data = $this->add_additional_fields_to_object( $data, $request );
114
 
115
+ // Wrap the data in a response object.
116
+ $response = rest_ensure_response( $data );
117
+
118
+ $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
119
+ $response->add_links( array(
120
+ 'collection' => array(
121
+ 'href' => rest_url( 'wp/v2/taxonomies' ),
122
+ ),
123
+ 'https://api.w.org/items' => array(
124
+ 'href' => rest_url( sprintf( 'wp/v2/%s', $base ) ),
125
+ ),
126
+ ) );
127
+
128
+ /**
129
+ * Filter a taxonomy returned from the API.
130
+ *
131
+ * Allows modification of the taxonomy data right before it is returned.
132
+ *
133
+ * @param WP_REST_Response $response The response object.
134
+ * @param object $item The original taxonomy object.
135
+ * @param WP_REST_Request $request Request used to generate the response.
136
+ */
137
+ return apply_filters( 'rest_prepare_taxonomy', $response, $taxonomy, $request );
138
  }
139
 
140
  /**
149
  'type' => 'object',
150
  'properties' => array(
151
  'description' => array(
152
+ 'description' => __( 'A human-readable description of the object.' ),
153
  'type' => 'string',
154
+ 'context' => array( 'view', 'edit' ),
155
  ),
156
  'hierarchical' => array(
157
+ 'description' => __( 'Whether or not the type should have children.' ),
158
  'type' => 'boolean',
159
+ 'context' => array( 'view', 'edit' ),
160
  ),
161
  'labels' => array(
162
+ 'description' => __( 'Human-readable labels for the type for various contexts.' ),
163
  'type' => 'object',
164
+ 'context' => array( 'edit' ),
165
  ),
166
  'name' => array(
167
+ 'description' => __( 'The title for the object.' ),
168
  'type' => 'string',
169
+ 'context' => array( 'view', 'edit' ),
170
  ),
171
  'slug' => array(
172
+ 'description' => __( 'An alphanumeric identifier for the object.' ),
173
  'type' => 'string',
174
+ 'context' => array( 'view', 'edit' ),
175
  ),
176
  'show_cloud' => array(
177
+ 'description' => __( 'Whether or not the term cloud should be displayed.' ),
178
  'type' => 'boolean',
179
+ 'context' => array( 'edit' ),
180
  ),
181
  'types' => array(
182
+ 'description' => __( 'Types associated with taxonomy.' ),
183
  'type' => 'array',
184
+ 'context' => array( 'view', 'edit' ),
185
  ),
186
  ),
187
  );
188
  return $this->add_additional_fields_schema( $schema );
189
  }
190
 
191
+ /**
192
+ * Get the query params for collections
193
+ *
194
+ * @return array
195
+ */
196
+ public function get_collection_params() {
197
+ $new_params = array();
198
+ $new_params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
199
+ $new_params['type'] = array(
200
+ 'description' => __( 'Limit results to taxonomies associated with a specific post type.' ),
201
+ 'type' => 'string',
202
+ );
203
+ return $new_params;
204
+ }
205
+
206
  }
lib/endpoints/class-wp-rest-terms-controller.php CHANGED
@@ -20,65 +20,44 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
20
  public function register_routes() {
21
 
22
  $base = $this->get_taxonomy_base( $this->taxonomy );
23
- $query_params = $this->get_collection_params();
24
- register_rest_route( 'wp/v2', '/terms/' . $base, array(
25
  array(
26
  'methods' => WP_REST_Server::READABLE,
27
  'callback' => array( $this, 'get_items' ),
28
  'permission_callback' => array( $this, 'get_items_permissions_check' ),
29
- 'args' => $query_params,
30
  ),
31
  array(
32
  'methods' => WP_REST_Server::CREATABLE,
33
  'callback' => array( $this, 'create_item' ),
34
  'permission_callback' => array( $this, 'create_item_permissions_check' ),
35
- 'args' => array(
36
- 'name' => array(
37
- 'required' => true,
38
- 'sanitize_callback' => 'sanitize_text_field',
39
- ),
40
- 'description' => array(
41
- 'sanitize_callback' => 'wp_filter_post_kses',
42
- ),
43
- 'slug' => array(
44
- 'sanitize_callback' => 'sanitize_title',
45
- ),
46
- 'parent' => array(),
47
- ),
48
  ),
 
 
49
  ));
50
- register_rest_route( 'wp/v2', '/terms/' . $base . '/(?P<id>[\d]+)', array(
51
  array(
52
  'methods' => WP_REST_Server::READABLE,
53
  'callback' => array( $this, 'get_item' ),
54
  'permission_callback' => array( $this, 'get_item_permissions_check' ),
 
 
 
55
  ),
56
  array(
57
  'methods' => WP_REST_Server::EDITABLE,
58
  'callback' => array( $this, 'update_item' ),
59
  'permission_callback' => array( $this, 'update_item_permissions_check' ),
60
- 'args' => array(
61
- 'name' => array(
62
- 'sanitize_callback' => 'sanitize_text_field',
63
- ),
64
- 'description' => array(
65
- 'sanitize_callback' => 'wp_filter_post_kses',
66
- ),
67
- 'slug' => array(
68
- 'sanitize_callback' => 'sanitize_title',
69
- ),
70
- 'parent' => array(),
71
- ),
72
  ),
73
  array(
74
  'methods' => WP_REST_Server::DELETABLE,
75
  'callback' => array( $this, 'delete_item' ),
76
  'permission_callback' => array( $this, 'delete_item_permissions_check' ),
77
  ),
78
- ) );
79
- register_rest_route( 'wp/v2', '/terms/' . $base . '/schema', array(
80
- 'methods' => WP_REST_Server::READABLE,
81
- 'callback' => array( $this, 'get_public_item_schema' ),
82
  ) );
83
  }
84
 
@@ -89,22 +68,44 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
89
  * @return WP_REST_Response|WP_Error
90
  */
91
  public function get_items( $request ) {
92
- $prepared_args = array( 'hide_empty' => false );
 
 
 
 
 
 
 
93
 
94
- $prepared_args['number'] = $request['per_page'];
95
  $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
96
- $prepared_args['search'] = $request['search'];
97
- $prepared_args['order'] = $request['order'];
98
- $prepared_args['orderby'] = $request['orderby'];
99
 
100
  $taxonomy_obj = get_taxonomy( $this->taxonomy );
 
101
  if ( $taxonomy_obj->hierarchical && isset( $request['parent'] ) ) {
102
- $parent = get_term_by( 'term_taxonomy_id', (int) $request['parent'], $this->taxonomy );
103
- if ( $parent ) {
104
- $prepared_args['parent'] = $parent->term_id;
 
 
 
 
105
  }
106
  }
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  $query_result = get_terms( $this->taxonomy, $prepared_args );
109
  $response = array();
110
  foreach ( $query_result as $term ) {
@@ -113,24 +114,35 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
113
  }
114
 
115
  $response = rest_ensure_response( $response );
 
 
 
 
116
  unset( $prepared_args['number'] );
117
  unset( $prepared_args['offset'] );
 
118
  $total_terms = wp_count_terms( $this->taxonomy, $prepared_args );
 
 
 
 
 
 
119
  $response->header( 'X-WP-Total', (int) $total_terms );
120
- $max_pages = ceil( $total_terms / $request['per_page'] );
121
  $response->header( 'X-WP-TotalPages', (int) $max_pages );
122
 
123
- $base = add_query_arg( $request->get_query_params(), rest_url( '/wp/v2/terms/' . $this->get_taxonomy_base( $this->taxonomy ) ) );
124
- if ( $request['page'] > 1 ) {
125
- $prev_page = $request['page'] - 1;
126
  if ( $prev_page > $max_pages ) {
127
  $prev_page = $max_pages;
128
  }
129
  $prev_link = add_query_arg( 'page', $prev_page, $base );
130
  $response->link_header( 'prev', $prev_link );
131
  }
132
- if ( $max_pages > $request['page'] ) {
133
- $next_page = $request['page'] + 1;
134
  $next_link = add_query_arg( 'page', $next_page, $base );
135
  $response->link_header( 'next', $next_link );
136
  }
@@ -146,8 +158,8 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
146
  */
147
  public function get_item( $request ) {
148
 
149
- $term = get_term_by( 'term_taxonomy_id', (int) $request['id'], $this->taxonomy );
150
- if ( ! $term ) {
151
  return new WP_Error( 'rest_term_invalid', __( "Term doesn't exist." ), array( 'status' => 404 ) );
152
  }
153
  if ( is_wp_error( $term ) ) {
@@ -182,7 +194,7 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
182
  return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Can not set term parent, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
183
  }
184
 
185
- $parent = get_term_by( 'term_taxonomy_id', (int) $request['parent'], $this->taxonomy );
186
 
187
  if ( ! $parent ) {
188
  return new WP_Error( 'rest_term_invalid', __( "Parent term doesn't exist." ), array( 'status' => 404 ) );
@@ -193,16 +205,28 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
193
 
194
  $term = wp_insert_term( $name, $this->taxonomy, $args );
195
  if ( is_wp_error( $term ) ) {
 
 
 
 
 
 
 
 
 
196
  return $term;
197
  }
198
 
199
  $this->update_additional_fields_for_object( $term, $request );
200
 
201
- $response = $this->get_item( array(
202
- 'id' => $term['term_taxonomy_id'],
203
- ) );
204
 
205
- return rest_ensure_response( $response );
 
 
 
206
  }
207
 
208
  /**
@@ -229,7 +253,7 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
229
  return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Can not set term parent, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
230
  }
231
 
232
- $parent = get_term_by( 'term_taxonomy_id', (int) $request['parent'], $this->taxonomy );
233
 
234
  if ( ! $parent ) {
235
  return new WP_Error( 'rest_term_invalid', __( "Parent term doesn't exist." ), array( 'status' => 400 ) );
@@ -238,10 +262,7 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
238
  $prepared_args['parent'] = $parent->term_id;
239
  }
240
 
241
- $term = get_term_by( 'term_taxonomy_id', (int) $request['id'], $this->taxonomy );
242
- if ( ! $term ) {
243
- return new WP_Error( 'rest_term_invalid', __( "Term doesn't exist." ), array( 'status' => 404 ) );
244
- }
245
 
246
  // Only update the term if we haz something to update.
247
  if ( ! empty( $prepared_args ) ) {
@@ -251,11 +272,11 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
251
  }
252
  }
253
 
254
- $this->update_additional_fields_for_object( get_term_by( 'term_taxonomy_id', (int) $request['id'], $this->taxonomy ), $request );
255
 
256
- $response = $this->get_item( array(
257
- 'id' => $term->term_taxonomy_id,
258
- ) );
259
 
260
  return rest_ensure_response( $response );
261
  }
@@ -264,16 +285,24 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
264
  * Delete a single term from a taxonomy
265
  *
266
  * @param WP_REST_Request $request Full details about the request
267
- * @return null
268
  */
269
  public function delete_item( $request ) {
270
 
271
  // Get the actual term_id
272
- $term = get_term_by( 'term_taxonomy_id', (int) $request['id'], $this->taxonomy );
273
- $get_request = new WP_REST_Request( 'GET', rest_url( 'wp/v2/terms/' . $this->get_taxonomy_base( $term->taxonomy ) . '/' . (int) $request['id'] ) );
 
274
  $get_request->set_param( 'context', 'view' );
275
  $response = $this->prepare_item_for_response( $term, $get_request );
276
 
 
 
 
 
 
 
 
277
  $retval = wp_delete_term( $term->term_id, $term->taxonomy );
278
  if ( ! $retval ) {
279
  return new WP_Error( 'rest_cannot_delete', __( 'The term cannot be deleted.' ), array( 'status' => 500 ) );
@@ -289,18 +318,7 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
289
  * @return bool|WP_Error
290
  */
291
  public function get_items_permissions_check( $request ) {
292
-
293
- $valid = $this->check_valid_taxonomy( $this->taxonomy );
294
- if ( is_wp_error( $valid ) ) {
295
- return $valid;
296
- }
297
-
298
- $tax_obj = get_taxonomy( $this->taxonomy );
299
- if ( $tax_obj && false === $tax_obj->public ) {
300
- return false;
301
- }
302
-
303
- return true;
304
  }
305
 
306
  /**
@@ -310,18 +328,7 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
310
  * @return bool|WP_Error
311
  */
312
  public function get_item_permissions_check( $request ) {
313
-
314
- $valid = $this->check_valid_taxonomy( $this->taxonomy );
315
- if ( is_wp_error( $valid ) ) {
316
- return $valid;
317
- }
318
-
319
- $tax_obj = get_taxonomy( $this->taxonomy );
320
- if ( $tax_obj && false === $tax_obj->public ) {
321
- return false;
322
- }
323
-
324
- return true;
325
  }
326
 
327
 
@@ -333,14 +340,13 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
333
  */
334
  public function create_item_permissions_check( $request ) {
335
 
336
- $valid = $this->check_valid_taxonomy( $this->taxonomy );
337
- if ( is_wp_error( $valid ) ) {
338
- return $valid;
339
  }
340
 
341
  $taxonomy_obj = get_taxonomy( $this->taxonomy );
342
  if ( ! current_user_can( $taxonomy_obj->cap->manage_terms ) ) {
343
- return false;
344
  }
345
 
346
  return true;
@@ -354,14 +360,18 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
354
  */
355
  public function update_item_permissions_check( $request ) {
356
 
357
- $valid = $this->check_valid_taxonomy( $this->taxonomy );
358
- if ( is_wp_error( $valid ) ) {
359
- return $valid;
 
 
 
 
360
  }
361
 
362
  $taxonomy_obj = get_taxonomy( $this->taxonomy );
363
- if ( $taxonomy_obj && ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) {
364
- return false;
365
  }
366
 
367
  return true;
@@ -375,19 +385,18 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
375
  */
376
  public function delete_item_permissions_check( $request ) {
377
 
378
- $valid = $this->check_valid_taxonomy( $this->taxonomy );
379
- if ( is_wp_error( $valid ) ) {
380
- return $valid;
381
  }
382
 
383
- $term = get_term_by( 'term_taxonomy_id', (int) $request['id'], $this->taxonomy );
384
  if ( ! $term ) {
385
  return new WP_Error( 'rest_term_invalid', __( "Term doesn't exist." ), array( 'status' => 404 ) );
386
  }
387
 
388
  $taxonomy_obj = get_taxonomy( $this->taxonomy );
389
- if ( $taxonomy_obj && ! current_user_can( $taxonomy_obj->cap->delete_terms ) ) {
390
- return false;
391
  }
392
 
393
  return true;
@@ -417,16 +426,8 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
417
  */
418
  public function prepare_item_for_response( $item, $request ) {
419
 
420
- $parent_id = 0;
421
- if ( $item->parent ) {
422
- $parent_term = get_term_by( 'id', (int) $item->parent, $item->taxonomy );
423
- if ( $parent_term ) {
424
- $parent_id = $parent_term->term_taxonomy_id;
425
- }
426
- }
427
-
428
  $data = array(
429
- 'id' => (int) $item->term_taxonomy_id,
430
  'count' => (int) $item->count,
431
  'description' => $item->description,
432
  'link' => get_term_link( $item ),
@@ -436,18 +437,27 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
436
  );
437
  $schema = $this->get_item_schema();
438
  if ( ! empty( $schema['properties']['parent'] ) ) {
439
- $data['parent'] = (int) $parent_id;
440
  }
441
 
442
  $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
443
  $data = $this->filter_response_by_context( $data, $context );
444
  $data = $this->add_additional_fields_to_object( $data, $request );
445
 
446
- $data = rest_ensure_response( $data );
447
 
448
- $data->add_links( $this->prepare_links( $item ) );
449
 
450
- return apply_filters( 'rest_prepare_term', $data, $item, $request );
 
 
 
 
 
 
 
 
 
451
  }
452
 
453
  /**
@@ -457,21 +467,24 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
457
  * @return array Links for the given term.
458
  */
459
  protected function prepare_links( $term ) {
460
- $base = '/wp/v2/terms/' . $this->get_taxonomy_base( $term->taxonomy );
461
  $links = array(
462
  'self' => array(
463
- 'href' => rest_url( trailingslashit( $base ) . $term->term_taxonomy_id ),
464
  ),
465
  'collection' => array(
466
  'href' => rest_url( $base ),
467
  ),
 
 
 
468
  );
469
 
470
  if ( $term->parent ) {
471
- $parent_term = get_term_by( 'id', (int) $term->parent, $term->taxonomy );
472
  if ( $parent_term ) {
473
  $links['up'] = array(
474
- 'href' => rest_url( sprintf( 'wp/v2/terms/%s/%d', $this->get_taxonomy_base( $parent_term->taxonomy ), $parent_term->term_taxonomy_id ) ),
475
  'embeddable' => true,
476
  );
477
  }
@@ -492,52 +505,62 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
492
  'type' => 'object',
493
  'properties' => array(
494
  'id' => array(
495
- 'description' => 'Unique identifier for the object.',
496
  'type' => 'integer',
497
  'context' => array( 'view', 'embed' ),
498
  'readonly' => true,
499
- ),
500
  'count' => array(
501
- 'description' => 'Number of published posts for the object.',
502
  'type' => 'integer',
503
  'context' => array( 'view' ),
504
  'readonly' => true,
505
- ),
506
  'description' => array(
507
- 'description' => 'A human-readable description of the object.',
508
  'type' => 'string',
509
  'context' => array( 'view' ),
 
 
510
  ),
 
511
  'link' => array(
512
- 'description' => 'URL to the object.',
513
  'type' => 'string',
514
  'format' => 'uri',
515
  'context' => array( 'view', 'embed' ),
516
  'readonly' => true,
517
- ),
518
  'name' => array(
519
- 'description' => 'The title for the object.',
520
  'type' => 'string',
521
  'context' => array( 'view', 'embed' ),
 
 
522
  ),
 
 
523
  'slug' => array(
524
- 'description' => 'An alphanumeric identifier for the object unique to its type.',
525
  'type' => 'string',
526
  'context' => array( 'view', 'embed' ),
 
 
527
  ),
 
528
  'taxonomy' => array(
529
- 'description' => 'Type attribution for the object.',
530
  'type' => 'string',
531
  'enum' => array_keys( get_taxonomies() ),
532
  'context' => array( 'view', 'embed' ),
533
  'readonly' => true,
534
- ),
535
  ),
536
- );
 
537
  $taxonomy = get_taxonomy( $this->taxonomy );
538
  if ( $taxonomy->hierarchical ) {
539
  $schema['properties']['parent'] = array(
540
- 'description' => 'The ID for the parent of the object.',
541
  'type' => 'integer',
542
  'context' => array( 'view' ),
543
  );
@@ -552,26 +575,50 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
552
  */
553
  public function get_collection_params() {
554
  $query_params = parent::get_collection_params();
555
- $query_params['order'] = array(
556
- 'description' => 'Order sort attribute ascending or descending.',
557
- 'type' => 'string',
558
- 'default' => 'asc',
559
- 'enum' => array( 'asc', 'desc' ),
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  );
561
- $query_params['orderby'] = array(
562
- 'description' => 'Sort collection by object attribute.',
563
- 'type' => 'string',
564
- 'default' => 'name',
565
- 'enum' => array(
 
566
  'id',
 
567
  'name',
568
  'slug',
 
 
 
 
569
  ),
570
  );
 
 
 
 
 
571
  $taxonomy = get_taxonomy( $this->taxonomy );
572
  if ( $taxonomy->hierarchical ) {
573
  $query_params['parent'] = array(
574
- 'description' => 'Limit result set to terms assigned to a specific parent term.',
575
  'type' => 'integer',
576
  'sanitize_callback' => 'absint',
577
  );
@@ -585,11 +632,11 @@ class WP_REST_Terms_Controller extends WP_REST_Controller {
585
  * @param string
586
  * @return bool|WP_Error
587
  */
588
- protected function check_valid_taxonomy( $taxonomy ) {
589
- if ( get_taxonomy( $taxonomy ) ) {
 
590
  return true;
591
  }
592
-
593
- return new WP_Error( 'rest_taxonomy_invalid', __( "Taxonomy doesn't exist" ), array( 'status' => 404 ) );
594
  }
595
  }
20
  public function register_routes() {
21
 
22
  $base = $this->get_taxonomy_base( $this->taxonomy );
23
+ register_rest_route( 'wp/v2', '/' . $base, array(
 
24
  array(
25
  'methods' => WP_REST_Server::READABLE,
26
  'callback' => array( $this, 'get_items' ),
27
  'permission_callback' => array( $this, 'get_items_permissions_check' ),
28
+ 'args' => $this->get_collection_params(),
29
  ),
30
  array(
31
  'methods' => WP_REST_Server::CREATABLE,
32
  'callback' => array( $this, 'create_item' ),
33
  'permission_callback' => array( $this, 'create_item_permissions_check' ),
34
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
 
 
 
 
 
 
 
 
 
 
 
 
35
  ),
36
+
37
+ 'schema' => array( $this, 'get_public_item_schema' ),
38
  ));
39
+ register_rest_route( 'wp/v2', '/' . $base . '/(?P<id>[\d]+)', array(
40
  array(
41
  'methods' => WP_REST_Server::READABLE,
42
  'callback' => array( $this, 'get_item' ),
43
  'permission_callback' => array( $this, 'get_item_permissions_check' ),
44
+ 'args' => array(
45
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
46
+ ),
47
  ),
48
  array(
49
  'methods' => WP_REST_Server::EDITABLE,
50
  'callback' => array( $this, 'update_item' ),
51
  'permission_callback' => array( $this, 'update_item_permissions_check' ),
52
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
 
 
 
 
 
 
 
 
 
 
 
53
  ),
54
  array(
55
  'methods' => WP_REST_Server::DELETABLE,
56
  'callback' => array( $this, 'delete_item' ),
57
  'permission_callback' => array( $this, 'delete_item_permissions_check' ),
58
  ),
59
+
60
+ 'schema' => array( $this, 'get_public_item_schema' ),
 
 
61
  ) );
62
  }
63
 
68
  * @return WP_REST_Response|WP_Error
69
  */
70
  public function get_items( $request ) {
71
+ $prepared_args = array(
72
+ 'include' => $request['include'],
73
+ 'order' => $request['order'],
74
+ 'orderby' => $request['orderby'],
75
+ 'hide_empty' => $request['hide_empty'],
76
+ 'number' => $request['per_page'],
77
+ 'search' => $request['search'],
78
+ );
79
 
 
80
  $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
 
 
 
81
 
82
  $taxonomy_obj = get_taxonomy( $this->taxonomy );
83
+
84
  if ( $taxonomy_obj->hierarchical && isset( $request['parent'] ) ) {
85
+ if ( 0 === $request['parent'] ) {
86
+ // Only query top-level terms.
87
+ $prepared_args['parent'] = 0;
88
+ } else {
89
+ if ( $request['parent'] ) {
90
+ $prepared_args['parent'] = $request['parent'];
91
+ }
92
  }
93
  }
94
 
95
+ /**
96
+ * Filter the query arguments, before passing them to `get_terms()`.
97
+ *
98
+ * Enables adding extra arguments or setting defaults for a terms
99
+ * collection request.
100
+ *
101
+ * @see https://developer.wordpress.org/reference/functions/get_terms/
102
+ *
103
+ * @param array $prepared_args Array of arguments to be
104
+ * passed to get_terms.
105
+ * @param WP_REST_Request $request The current request.
106
+ */
107
+ $prepared_args = apply_filters( 'rest_terms_query', $prepared_args, $request );
108
+
109
  $query_result = get_terms( $this->taxonomy, $prepared_args );
110
  $response = array();
111
  foreach ( $query_result as $term ) {
114
  }
115
 
116
  $response = rest_ensure_response( $response );
117
+
118
+ // Store pagation values for headers then unset for count query.
119
+ $per_page = (int) $prepared_args['number'];
120
+ $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
121
  unset( $prepared_args['number'] );
122
  unset( $prepared_args['offset'] );
123
+
124
  $total_terms = wp_count_terms( $this->taxonomy, $prepared_args );
125
+
126
+ // wp_count_terms can return a falsy value when the term has no children
127
+ if ( ! $total_terms ) {
128
+ $total_terms = 0;
129
+ }
130
+
131
  $response->header( 'X-WP-Total', (int) $total_terms );
132
+ $max_pages = ceil( $total_terms / $per_page );
133
  $response->header( 'X-WP-TotalPages', (int) $max_pages );
134
 
135
+ $base = add_query_arg( $request->get_query_params(), rest_url( '/wp/v2/' . $this->get_taxonomy_base( $this->taxonomy ) ) );
136
+ if ( $page > 1 ) {
137
+ $prev_page = $page - 1;
138
  if ( $prev_page > $max_pages ) {
139
  $prev_page = $max_pages;
140
  }
141
  $prev_link = add_query_arg( 'page', $prev_page, $base );
142
  $response->link_header( 'prev', $prev_link );
143
  }
144
+ if ( $max_pages > $page ) {
145
+ $next_page = $page + 1;
146
  $next_link = add_query_arg( 'page', $next_page, $base );
147
  $response->link_header( 'next', $next_link );
148
  }
158
  */
159
  public function get_item( $request ) {
160
 
161
+ $term = get_term( (int) $request['id'], $this->taxonomy );
162
+ if ( ! $term || $term->taxonomy !== $this->taxonomy ) {
163
  return new WP_Error( 'rest_term_invalid', __( "Term doesn't exist." ), array( 'status' => 404 ) );
164
  }
165
  if ( is_wp_error( $term ) ) {
194
  return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Can not set term parent, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
195
  }
196
 
197
+ $parent = get_term( (int) $request['parent'], $this->taxonomy );
198
 
199
  if ( ! $parent ) {
200
  return new WP_Error( 'rest_term_invalid', __( "Parent term doesn't exist." ), array( 'status' => 404 ) );
205
 
206
  $term = wp_insert_term( $name, $this->taxonomy, $args );
207
  if ( is_wp_error( $term ) ) {
208
+
209
+ // If we're going to inform the client that the term exists, give them the identifier
210
+ // they can actually use.
211
+
212
+ if ( ( $term_id = $term->get_error_data( 'term_exists' ) ) ) {
213
+ $existing_term = get_term( $term_id, $this->taxonomy );
214
+ $term->add_data( $existing_term->term_id, 'term_exists' );
215
+ }
216
+
217
  return $term;
218
  }
219
 
220
  $this->update_additional_fields_for_object( $term, $request );
221
 
222
+ $get_request = new WP_REST_Request;
223
+ $get_request->set_param( 'id', $term['term_id'] );
224
+ $response = $this->get_item( $get_request );
225
 
226
+ $response = rest_ensure_response( $response );
227
+ $response->set_status( 201 );
228
+ $response->header( 'Location', rest_url( '/wp/v2/' . $this->get_taxonomy_base( $this->taxonomy ) . '/' . $term['term_id'] ) );
229
+ return $response;
230
  }
231
 
232
  /**
253
  return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Can not set term parent, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
254
  }
255
 
256
+ $parent = get_term( (int) $request['parent'], $this->taxonomy );
257
 
258
  if ( ! $parent ) {
259
  return new WP_Error( 'rest_term_invalid', __( "Parent term doesn't exist." ), array( 'status' => 400 ) );
262
  $prepared_args['parent'] = $parent->term_id;
263
  }
264
 
265
+ $term = get_term( (int) $request['id'], $this->taxonomy );
 
 
 
266
 
267
  // Only update the term if we haz something to update.
268
  if ( ! empty( $prepared_args ) ) {
272
  }
273
  }
274
 
275
+ $this->update_additional_fields_for_object( get_term( (int) $request['id'], $this->taxonomy ), $request );
276
 
277
+ $get_request = new WP_REST_Request;
278
+ $get_request->set_param( 'id', (int) $request['id'] );
279
+ $response = $this->get_item( $get_request );
280
 
281
  return rest_ensure_response( $response );
282
  }
285
  * Delete a single term from a taxonomy
286
  *
287
  * @param WP_REST_Request $request Full details about the request
288
+ * @return WP_REST_Response|WP_Error
289
  */
290
  public function delete_item( $request ) {
291
 
292
  // Get the actual term_id
293
+ $term = get_term( (int) $request['id'], $this->taxonomy );
294
+ $get_request = new WP_REST_Request;
295
+ $get_request->set_param( 'id', (int) $request['id'] );
296
  $get_request->set_param( 'context', 'view' );
297
  $response = $this->prepare_item_for_response( $term, $get_request );
298
 
299
+ $data = $response->get_data();
300
+ $data = array(
301
+ 'data' => $data,
302
+ 'deleted' => true,
303
+ );
304
+ $response->set_data( $data );
305
+
306
  $retval = wp_delete_term( $term->term_id, $term->taxonomy );
307
  if ( ! $retval ) {
308
  return new WP_Error( 'rest_cannot_delete', __( 'The term cannot be deleted.' ), array( 'status' => 500 ) );
318
  * @return bool|WP_Error
319
  */
320
  public function get_items_permissions_check( $request ) {
321
+ return $this->check_is_taxonomy_allowed( $this->taxonomy );
 
 
 
 
 
 
 
 
 
 
 
322
  }
323
 
324
  /**
328
  * @return bool|WP_Error
329
  */
330
  public function get_item_permissions_check( $request ) {
331
+ return $this->check_is_taxonomy_allowed( $this->taxonomy );
 
 
 
 
 
 
 
 
 
 
 
332
  }
333
 
334
 
340
  */
341
  public function create_item_permissions_check( $request ) {
342
 
343
+ if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
344
+ return false;
 
345
  }
346
 
347
  $taxonomy_obj = get_taxonomy( $this->taxonomy );
348
  if ( ! current_user_can( $taxonomy_obj->cap->manage_terms ) ) {
349
+ return new WP_Error( 'rest_cannot_create', __( 'Sorry, you cannot create new terms.' ), array( 'status' => rest_authorization_required_code() ) );
350
  }
351
 
352
  return true;
360
  */
361
  public function update_item_permissions_check( $request ) {
362
 
363
+ if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
364
+ return false;
365
+ }
366
+
367
+ $term = get_term( (int) $request['id'], $this->taxonomy );
368
+ if ( ! $term ) {
369
+ return new WP_Error( 'rest_term_invalid', __( "Term doesn't exist." ), array( 'status' => 404 ) );
370
  }
371
 
372
  $taxonomy_obj = get_taxonomy( $this->taxonomy );
373
+ if ( ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) {
374
+ return new WP_Error( 'rest_cannot_update', __( 'Sorry, you cannot update terms.' ), array( 'status' => rest_authorization_required_code() ) );
375
  }
376
 
377
  return true;
385
  */
386
  public function delete_item_permissions_check( $request ) {
387
 
388
+ if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
389
+ return false;
 
390
  }
391
 
392
+ $term = get_term( (int) $request['id'], $this->taxonomy );
393
  if ( ! $term ) {
394
  return new WP_Error( 'rest_term_invalid', __( "Term doesn't exist." ), array( 'status' => 404 ) );
395
  }
396
 
397
  $taxonomy_obj = get_taxonomy( $this->taxonomy );
398
+ if ( ! current_user_can( $taxonomy_obj->cap->delete_terms ) ) {
399
+ return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you cannot delete terms.' ), array( 'status' => rest_authorization_required_code() ) );
400
  }
401
 
402
  return true;
426
  */
427
  public function prepare_item_for_response( $item, $request ) {
428
 
 
 
 
 
 
 
 
 
429
  $data = array(
430
+ 'id' => (int) $item->term_id,
431
  'count' => (int) $item->count,
432
  'description' => $item->description,
433
  'link' => get_term_link( $item ),
437
  );
438
  $schema = $this->get_item_schema();
439
  if ( ! empty( $schema['properties']['parent'] ) ) {
440
+ $data['parent'] = (int) $item->parent;
441
  }
442
 
443
  $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
444
  $data = $this->filter_response_by_context( $data, $context );
445
  $data = $this->add_additional_fields_to_object( $data, $request );
446
 
447
+ $response = rest_ensure_response( $data );
448
 
449
+ $response->add_links( $this->prepare_links( $item ) );
450
 
451
+ /**
452
+ * Filter a term item returned from the API.
453
+ *
454
+ * Allows modification of the term data right before it is returned.
455
+ *
456
+ * @param WP_REST_Response $response The response object.
457
+ * @param object $item The original term object.
458
+ * @param WP_REST_Request $request Request used to generate the response.
459
+ */
460
+ return apply_filters( 'rest_prepare_term', $response, $item, $request );
461
  }
462
 
463
  /**
467
  * @return array Links for the given term.
468
  */
469
  protected function prepare_links( $term ) {
470
+ $base = '/wp/v2/' . $this->get_taxonomy_base( $term->taxonomy );
471
  $links = array(
472
  'self' => array(
473
+ 'href' => rest_url( trailingslashit( $base ) . $term->term_id ),
474
  ),
475
  'collection' => array(
476
  'href' => rest_url( $base ),
477
  ),
478
+ 'about' => array(
479
+ 'href' => rest_url( sprintf( 'wp/v2/taxonomies/%s', $this->taxonomy ) ),
480
+ ),
481
  );
482
 
483
  if ( $term->parent ) {
484
+ $parent_term = get_term( (int) $term->parent, $term->taxonomy );
485
  if ( $parent_term ) {
486
  $links['up'] = array(
487
+ 'href' => rest_url( sprintf( 'wp/v2/%s/%d', $this->get_taxonomy_base( $parent_term->taxonomy ), $parent_term->term_id ) ),
488
  'embeddable' => true,
489
  );
490
  }
505
  'type' => 'object',
506
  'properties' => array(
507
  'id' => array(
508
+ 'description' => __( 'Unique identifier for the object.' ),
509
  'type' => 'integer',
510
  'context' => array( 'view', 'embed' ),
511
  'readonly' => true,
512
+ ),
513
  'count' => array(
514
+ 'description' => __( 'Number of published posts for the object.' ),
515
  'type' => 'integer',
516
  'context' => array( 'view' ),
517
  'readonly' => true,
518
+ ),
519
  'description' => array(
520
+ 'description' => __( 'A human-readable description of the object.' ),
521
  'type' => 'string',
522
  'context' => array( 'view' ),
523
+ 'arg_options' => array(
524
+ 'sanitize_callback' => 'wp_filter_post_kses',
525
  ),
526
+ ),
527
  'link' => array(
528
+ 'description' => __( 'URL to the object.' ),
529
  'type' => 'string',
530
  'format' => 'uri',
531
  'context' => array( 'view', 'embed' ),
532
  'readonly' => true,
533
+ ),
534
  'name' => array(
535
+ 'description' => __( 'The title for the object.' ),
536
  'type' => 'string',
537
  'context' => array( 'view', 'embed' ),
538
+ 'arg_options' => array(
539
+ 'sanitize_callback' => 'sanitize_text_field',
540
  ),
541
+ 'required' => true,
542
+ ),
543
  'slug' => array(
544
+ 'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
545
  'type' => 'string',
546
  'context' => array( 'view', 'embed' ),
547
+ 'arg_options' => array(
548
+ 'sanitize_callback' => 'sanitize_title',
549
  ),
550
+ ),
551
  'taxonomy' => array(
552
+ 'description' => __( 'Type attribution for the object.' ),
553
  'type' => 'string',
554
  'enum' => array_keys( get_taxonomies() ),
555
  'context' => array( 'view', 'embed' ),
556
  'readonly' => true,
 
557
  ),
558
+ ),
559
+ );
560
  $taxonomy = get_taxonomy( $this->taxonomy );
561
  if ( $taxonomy->hierarchical ) {
562
  $schema['properties']['parent'] = array(
563
+ 'description' => __( 'The id for the parent of the object.' ),
564
  'type' => 'integer',
565
  'context' => array( 'view' ),
566
  );
575
  */
576
  public function get_collection_params() {
577
  $query_params = parent::get_collection_params();
578
+
579
+ $query_params['context']['default'] = 'view';
580
+
581
+ $query_params['include'] = array(
582
+ 'description' => __( 'Limit result set to specific ids.' ),
583
+ 'type' => 'array',
584
+ 'default' => array(),
585
+ 'sanitize_callback' => 'wp_parse_id_list',
586
+ );
587
+ $query_params['order'] = array(
588
+ 'description' => __( 'Order sort attribute ascending or descending.' ),
589
+ 'type' => 'string',
590
+ 'sanitize_callback' => 'sanitize_key',
591
+ 'default' => 'asc',
592
+ 'enum' => array(
593
+ 'asc',
594
+ 'desc',
595
+ ),
596
  );
597
+ $query_params['orderby'] = array(
598
+ 'description' => __( 'Sort collection by object attribute.' ),
599
+ 'type' => 'string',
600
+ 'sanitize_callback' => 'sanitize_key',
601
+ 'default' => 'name',
602
+ 'enum' => array(
603
  'id',
604
+ 'include',
605
  'name',
606
  'slug',
607
+ 'term_group',
608
+ 'term_id',
609
+ 'description',
610
+ 'count',
611
  ),
612
  );
613
+ $query_params['hide_empty'] = array(
614
+ 'description' => __( 'Whether to hide terms not assigned to any posts.' ),
615
+ 'type' => 'boolean',
616
+ 'default' => false,
617
+ );
618
  $taxonomy = get_taxonomy( $this->taxonomy );
619
  if ( $taxonomy->hierarchical ) {
620
  $query_params['parent'] = array(
621
+ 'description' => __( 'Limit result set to terms assigned to a specific parent term.' ),
622
  'type' => 'integer',
623
  'sanitize_callback' => 'absint',
624
  );
632
  * @param string
633
  * @return bool|WP_Error
634
  */
635
+ protected function check_is_taxonomy_allowed( $taxonomy ) {
636
+ $taxonomy_obj = get_taxonomy( $taxonomy );
637
+ if ( $taxonomy_obj && ! empty( $taxonomy_obj->show_in_rest ) ) {
638
  return true;
639
  }
640
+ return false;
 
641
  }
642
  }
lib/endpoints/class-wp-rest-users-controller.php CHANGED
@@ -10,24 +10,24 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
10
  */
11
  public function register_routes() {
12
 
13
- $query_params = $this->get_collection_params();
14
  register_rest_route( 'wp/v2', '/users', array(
15
  array(
16
  'methods' => WP_REST_Server::READABLE,
17
  'callback' => array( $this, 'get_items' ),
18
- 'permission_callback' => array( $this, 'get_items_permissions_check' ),
19
- 'args' => $query_params,
20
  ),
21
  array(
22
  'methods' => WP_REST_Server::CREATABLE,
23
  'callback' => array( $this, 'create_item' ),
24
  'permission_callback' => array( $this, 'create_item_permissions_check' ),
25
- 'args' => array_merge( $this->get_endpoint_args_for_item_schema( true ), array(
26
  'password' => array(
27
  'required' => true,
28
  ),
29
  ) ),
30
  ),
 
 
31
  ) );
32
  register_rest_route( 'wp/v2', '/users/(?P<id>[\d]+)', array(
33
  array(
@@ -35,16 +35,14 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
35
  'callback' => array( $this, 'get_item' ),
36
  'permission_callback' => array( $this, 'get_item_permissions_check' ),
37
  'args' => array(
38
- 'context' => array(
39
- 'default' => 'embed',
40
- ),
41
  ),
42
  ),
43
  array(
44
  'methods' => WP_REST_Server::EDITABLE,
45
  'callback' => array( $this, 'update_item' ),
46
  'permission_callback' => array( $this, 'update_item_permissions_check' ),
47
- 'args' => array_merge( $this->get_endpoint_args_for_item_schema( false ), array(
48
  'password' => array(),
49
  ) ),
50
  ),
@@ -53,9 +51,14 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
53
  'callback' => array( $this, 'delete_item' ),
54
  'permission_callback' => array( $this, 'delete_item_permissions_check' ),
55
  'args' => array(
 
 
 
56
  'reassign' => array(),
57
  ),
58
  ),
 
 
59
  ) );
60
 
61
  register_rest_route( 'wp/v2', '/users/me', array(
@@ -64,12 +67,8 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
64
  'args' => array(
65
  'context' => array(),
66
  ),
 
67
  ));
68
-
69
- register_rest_route( 'wp/v2', '/users/schema', array(
70
- 'methods' => WP_REST_Server::READABLE,
71
- 'callback' => array( $this, 'get_public_item_schema' ),
72
- ) );
73
  }
74
 
75
  /**
@@ -81,17 +80,38 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
81
  public function get_items( $request ) {
82
 
83
  $prepared_args = array();
 
84
  $prepared_args['order'] = $request['order'];
85
  $prepared_args['number'] = $request['per_page'];
86
  $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
87
  $orderby_possibles = array(
88
  'id' => 'ID',
 
89
  'name' => 'display_name',
90
  'registered_date' => 'registered',
91
- );
92
  $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
93
  $prepared_args['search'] = $request['search'];
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  $prepared_args = apply_filters( 'rest_user_query', $prepared_args, $request );
96
 
97
  $query = new WP_User_Query( $prepared_args );
@@ -106,25 +126,32 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
106
  }
107
 
108
  $response = rest_ensure_response( $users );
 
 
 
 
109
  unset( $prepared_args['number'] );
110
  unset( $prepared_args['offset'] );
 
 
 
111
  $count_query = new WP_User_Query( $prepared_args );
112
  $total_users = $count_query->get_total();
113
  $response->header( 'X-WP-Total', (int) $total_users );
114
- $max_pages = ceil( $total_users / $request['per_page'] );
115
  $response->header( 'X-WP-TotalPages', (int) $max_pages );
116
 
117
  $base = add_query_arg( $request->get_query_params(), rest_url( '/wp/v2/users' ) );
118
- if ( $request['page'] > 1 ) {
119
- $prev_page = $request['page'] - 1;
120
  if ( $prev_page > $max_pages ) {
121
  $prev_page = $max_pages;
122
  }
123
  $prev_link = add_query_arg( 'page', $prev_page, $base );
124
  $response->link_header( 'prev', $prev_link );
125
  }
126
- if ( $max_pages > $request['page'] ) {
127
- $next_page = $request['page'] + 1;
128
  $next_link = add_query_arg( 'page', $next_page, $base );
129
  $response->link_header( 'next', $next_link );
130
  }
@@ -143,7 +170,7 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
143
  $user = get_userdata( $id );
144
 
145
  if ( empty( $id ) || empty( $user->ID ) ) {
146
- return new WP_Error( 'rest_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) );
147
  }
148
 
149
  $user = $this->prepare_item_for_response( $user, $request );
@@ -164,10 +191,10 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
164
  return new WP_Error( 'rest_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) );
165
  }
166
 
167
- $response = $this->get_item( array(
168
- 'id' => $current_user_id,
169
- 'context' => $request['context'],
170
- ));
171
  if ( is_wp_error( $response ) ) {
172
  return $response;
173
  }
@@ -192,10 +219,6 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
192
  return new WP_Error( 'rest_user_exists', __( 'Cannot create existing user.' ), array( 'status' => 400 ) );
193
  }
194
 
195
- if ( ! empty( $request['role'] ) && ! isset( $wp_roles->role_objects[ $request['role'] ] ) ) {
196
- return new WP_Error( 'rest_user_invalid_role', __( 'Role is invalid.' ), array( 'status' => 400 ) );
197
- }
198
-
199
  $user = $this->prepare_item_for_database( $request );
200
 
201
  if ( is_multisite() ) {
@@ -225,12 +248,19 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
225
 
226
  $this->update_additional_fields_for_object( $user, $request );
227
 
228
- do_action( 'rest_insert_user', $user, $request, false );
229
-
230
- $response = $this->get_item( array(
231
- 'id' => $user_id,
232
- 'context' => 'edit',
233
- ));
 
 
 
 
 
 
 
234
  $response = rest_ensure_response( $response );
235
  $response->set_status( 201 );
236
  $response->header( 'Location', rest_url( '/wp/v2/users/' . $user_id ) );
@@ -249,7 +279,7 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
249
 
250
  $user = get_userdata( $id );
251
  if ( ! $user ) {
252
- return new WP_Error( 'rest_user_invalid_id', __( 'User ID is invalid.' ), array( 'status' => 400 ) );
253
  }
254
 
255
  if ( email_exists( $request['email'] ) && $request['email'] !== $user->user_email ) {
@@ -283,16 +313,15 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
283
 
284
  $this->update_additional_fields_for_object( $user, $request );
285
 
 
286
  do_action( 'rest_insert_user', $user, $request, false );
287
 
288
- $response = $this->get_item( array(
289
- 'id' => $user_id,
290
- 'context' => 'edit',
291
- ));
292
- $response = rest_ensure_response( $response );
293
- $response->header( 'Location', rest_url( '/wp/v2/users/' . $user_id ) );
294
 
295
- return $response;
296
  }
297
 
298
  /**
@@ -308,46 +337,50 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
308
 
309
  // We don't support trashing for this type, error out
310
  if ( ! $force ) {
311
- return new WP_Error( 'rest_trash_not_supported', __( 'Terms do not support trashing.' ), array( 'status' => 501 ) );
312
  }
313
 
314
  $user = get_userdata( $id );
315
  if ( ! $user ) {
316
- return new WP_Error( 'rest_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 400 ) );
317
  }
318
 
319
  if ( ! empty( $reassign ) ) {
320
  if ( $reassign === $id || ! get_userdata( $reassign ) ) {
321
- return new WP_Error( 'rest_user_invalid_reassign', __( 'Invalid user ID.' ), array( 'status' => 400 ) );
322
  }
323
  }
324
 
325
- $get_request = new WP_REST_Request( 'GET', rest_url( 'wp/v2/users/' . $id ) );
 
326
  $get_request->set_param( 'context', 'edit' );
327
  $orig_user = $this->prepare_item_for_response( $user, $get_request );
328
 
 
 
 
 
 
 
 
 
 
 
329
  $result = wp_delete_user( $id, $reassign );
330
 
331
  if ( ! $result ) {
332
  return new WP_Error( 'rest_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 500 ) );
333
  }
334
 
335
- return $orig_user;
336
- }
337
-
338
- /**
339
- * Check if a given request has access to list users
340
- *
341
- * @param WP_REST_Request $request Full details about the request.
342
- * @return bool
343
- */
344
- public function get_items_permissions_check( $request ) {
345
 
346
- if ( ! current_user_can( 'list_users' ) ) {
347
- return false;
348
- }
349
-
350
- return true;
351
  }
352
 
353
  /**
@@ -362,7 +395,7 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
362
  $user = get_userdata( $id );
363
 
364
  if ( empty( $id ) || empty( $user->ID ) ) {
365
- return new WP_Error( 'rest_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) );
366
  }
367
 
368
  if ( get_current_user_id() === $id ) {
@@ -372,11 +405,11 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
372
  $context = ! empty( $request['context'] ) && in_array( $request['context'], array( 'edit', 'view', 'embed' ) ) ? $request['context'] : 'embed';
373
 
374
  if ( 'edit' === $context && ! current_user_can( 'edit_user', $id ) ) {
375
- return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this user with edit context' ), array( 'status' => 403 ) );
376
  } else if ( 'view' === $context && ! current_user_can( 'list_users' ) ) {
377
- return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this user with view context' ), array( 'status' => 403 ) );
378
  } else if ( 'embed' === $context && ! count_user_posts( $id ) && ! current_user_can( 'edit_user', $id ) && ! current_user_can( 'list_users' ) ) {
379
- return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this user' ), array( 'status' => 403 ) );
380
  }
381
 
382
  return true;
@@ -391,7 +424,7 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
391
  public function create_item_permissions_check( $request ) {
392
 
393
  if ( ! current_user_can( 'create_users' ) ) {
394
- return new WP_Error( 'rest_cannot_create_user', __( 'Sorry, you are not allowed to create users.' ), array( 'status' => 403 ) );
395
  }
396
 
397
  return true;
@@ -408,11 +441,11 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
408
  $id = (int) $request['id'];
409
 
410
  if ( ! current_user_can( 'edit_user', $id ) ) {
411
- return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit users.' ), array( 'status' => 403 ) );
412
  }
413
 
414
  if ( ! empty( $request['role'] ) && ! current_user_can( 'edit_users' ) ) {
415
- return new WP_Error( 'rest_cannot_edit_roles', __( 'Sorry, you are not allowed to edit roles of users.' ), array( 'status' => 403 ) );
416
  }
417
 
418
  return true;
@@ -430,7 +463,7 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
430
  $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null;
431
 
432
  if ( ! current_user_can( 'delete_user', $id ) ) {
433
- return new WP_Error( 'rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this user.' ), array( 'status' => 403 ) );
434
  }
435
 
436
  return true;
@@ -441,26 +474,26 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
441
  *
442
  * @param object $user User object.
443
  * @param WP_REST_Request $request Request object.
444
- * @return array $data Response data.
445
  */
446
  public function prepare_item_for_response( $user, $request ) {
447
  $data = array(
448
- 'avatar_urls' => rest_get_avatar_urls( $user->user_email ),
449
- 'capabilities' => $user->allcaps,
450
- 'description' => $user->description,
451
- 'email' => $user->user_email,
452
- 'extra_capabilities' => $user->caps,
453
- 'first_name' => $user->first_name,
454
  'id' => $user->ID,
 
 
 
455
  'last_name' => $user->last_name,
 
 
 
456
  'link' => get_author_posts_url( $user->ID ),
457
- 'name' => $user->display_name,
458
  'nickname' => $user->nickname,
 
459
  'registered_date' => date( 'c', strtotime( $user->user_registered ) ),
460
  'roles' => $user->roles,
461
- 'slug' => $user->user_nicename,
462
- 'url' => $user->user_url,
463
- 'username' => $user->user_login,
464
  );
465
 
466
  $context = ! empty( $request['context'] ) ? $request['context'] : 'embed';
@@ -469,11 +502,18 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
469
  $data = $this->add_additional_fields_to_object( $data, $request );
470
 
471
  // Wrap the data in a response object
472
- $data = rest_ensure_response( $data );
473
-
474
- $data->add_links( $this->prepare_links( $user ) );
475
-
476
- return apply_filters( 'rest_prepare_user', $data, $user, $request );
 
 
 
 
 
 
 
477
  }
478
 
479
  /**
@@ -538,12 +578,18 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
538
  $prepared_user->description = $request['description'];
539
  }
540
  if ( isset( $request['role'] ) ) {
541
- $prepared_user->role = sanitize_text_field( $request['role'] );
542
  }
543
  if ( isset( $request['url'] ) ) {
544
  $prepared_user->user_url = $request['url'];
545
  }
546
 
 
 
 
 
 
 
547
  return apply_filters( 'rest_pre_insert_user', $prepared_user, $request );
548
  }
549
 
@@ -557,16 +603,16 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
557
  protected function check_role_update( $user_id, $role ) {
558
  global $wp_roles;
559
 
560
- if ( ! isset( $wp_roles->role_objects[ $role ] ) ) {
561
- return new WP_Error( 'rest_user_invalid_role', __( 'Role is invalid.' ), array( 'status' => 400 ) );
562
- }
563
-
564
  $potential_role = $wp_roles->role_objects[ $role ];
565
 
566
  // Don't let anyone with 'edit_users' (admins) edit their own role to something without it.
567
  // Multisite super admins can freely edit their blog roles -- they possess all caps.
568
  if ( ( is_multisite() && current_user_can( 'manage_sites' ) ) || get_current_user_id() !== $user_id || $potential_role->has_cap( 'edit_users' ) ) {
569
  // The new role must be editable by the logged-in user.
 
 
 
 
570
  $editable_roles = get_editable_roles();
571
  if ( empty( $editable_roles[ $role ] ) ) {
572
  return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give users that role.' ), array( 'status' => 403 ) );
@@ -575,7 +621,7 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
575
  return true;
576
  }
577
 
578
- return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give users that role.' ), array( 'status' => 403 ) );
579
  }
580
 
581
  /**
@@ -589,129 +635,138 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
589
  $avatar_sizes = rest_get_avatar_sizes();
590
  foreach ( $avatar_sizes as $size ) {
591
  $avatar_properties[ $size ] = array(
592
- 'description' => 'Avatar URL with image size of ' . $size . ' pixels.',
593
- 'type' => 'uri',
 
594
  'context' => array( 'embed', 'view', 'edit' ),
595
  );
596
  }
597
 
 
 
598
  $schema = array(
599
  '$schema' => 'http://json-schema.org/draft-04/schema#',
600
  'title' => 'user',
601
  'type' => 'object',
602
  'properties' => array(
603
- 'avatar_urls' => array(
604
- 'description' => 'Avatar URLs for the object.',
605
- 'type' => 'object',
606
  'context' => array( 'embed', 'view', 'edit' ),
607
  'readonly' => true,
608
- 'properties' => $avatar_properties,
609
  ),
610
- 'capabilities' => array(
611
- 'description' => 'All capabilities assigned to the user.',
612
- 'type' => 'object',
613
- 'context' => array( 'view', 'edit' ),
614
- ),
615
- 'description' => array(
616
- 'description' => 'Description of the object.',
617
  'type' => 'string',
618
- 'context' => array( 'embed', 'view', 'edit' ),
 
619
  'arg_options' => array(
620
- 'sanitize_callback' => 'wp_filter_post_kses',
621
  ),
622
  ),
623
- 'email' => array(
624
- 'description' => 'The email address for the object.',
625
  'type' => 'string',
626
- 'format' => 'email',
627
- 'context' => array( 'view', 'edit' ),
628
- 'required' => true,
629
- ),
630
- 'extra_capabilities' => array(
631
- 'description' => 'Any extra capabilities assigned to the user.',
632
- 'type' => 'object',
633
- 'context' => array( 'edit' ),
634
- 'readonly' => true,
635
  ),
 
636
  'first_name' => array(
637
- 'description' => 'First name for the object.',
638
  'type' => 'string',
639
  'context' => array( 'view', 'edit' ),
640
  'arg_options' => array(
641
  'sanitize_callback' => 'sanitize_text_field',
642
  ),
643
  ),
644
- 'id' => array(
645
- 'description' => 'Unique identifier for the object.',
646
- 'type' => 'integer',
647
- 'context' => array( 'embed', 'view', 'edit' ),
648
- 'readonly' => true,
649
- ),
650
  'last_name' => array(
651
- 'description' => 'Last name for the object.',
652
  'type' => 'string',
653
  'context' => array( 'view', 'edit' ),
654
  'arg_options' => array(
655
  'sanitize_callback' => 'sanitize_text_field',
656
  ),
657
  ),
658
- 'link' => array(
659
- 'description' => 'Author URL to the object.',
 
 
 
 
 
 
 
660
  'type' => 'string',
661
  'format' => 'uri',
662
  'context' => array( 'embed', 'view', 'edit' ),
663
  'readonly' => true,
664
  ),
665
- 'name' => array(
666
- 'description' => 'Display name for the object.',
667
  'type' => 'string',
668
  'context' => array( 'embed', 'view', 'edit' ),
669
  'arg_options' => array(
670
- 'sanitize_callback' => 'sanitize_text_field',
671
  ),
672
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
  'nickname' => array(
674
- 'description' => 'The nickname for the object.',
675
  'type' => 'string',
676
  'context' => array( 'view', 'edit' ),
677
  'arg_options' => array(
678
  'sanitize_callback' => 'sanitize_text_field',
679
  ),
680
  ),
 
 
 
 
 
 
 
 
681
  'registered_date' => array(
682
- 'description' => 'Registration date for the user.',
683
  'type' => 'date-time',
684
  'context' => array( 'view', 'edit' ),
685
  'readonly' => true,
686
  ),
687
  'roles' => array(
688
- 'description' => 'Roles assigned to the user.',
689
  'type' => 'array',
690
  'context' => array( 'view', 'edit' ),
 
691
  ),
692
- 'slug' => array(
693
- 'description' => 'An alphanumeric identifier for the object unique to its type.',
694
  'type' => 'string',
695
- 'context' => array( 'view', 'edit' ),
696
- 'arg_options' => array(
697
- 'sanitize_callback' => 'sanitize_title',
698
- ),
699
  ),
700
- 'url' => array(
701
- 'description' => 'URL of the object.',
702
- 'type' => 'string',
703
- 'format' => 'uri',
704
- 'context' => array( 'embed', 'view', 'edit' ),
705
- 'readonly' => true,
706
  ),
707
- 'username' => array(
708
- 'description' => 'Login name for the user.',
709
- 'type' => 'string',
710
  'context' => array( 'edit' ),
711
- 'required' => true,
712
- 'arg_options' => array(
713
- 'sanitize_callback' => 'sanitize_user',
714
- ),
715
  ),
716
  ),
717
  );
@@ -725,24 +780,31 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
725
  */
726
  public function get_collection_params() {
727
  $query_params = parent::get_collection_params();
728
- $query_params['context'] = array(
729
- 'default' => 'view',
730
- 'description' => 'Change the response format based on request context.',
731
- 'enum' => array( 'view', 'edit' ),
732
- 'sanitize_callback' => 'sanitize_key',
733
- 'type' => 'string',
 
 
734
  );
735
  $query_params['order'] = array(
736
  'default' => 'asc',
737
- 'description' => 'Order sort attribute ascending or descending.',
738
  'enum' => array( 'asc', 'desc' ),
739
  'sanitize_callback' => 'sanitize_key',
740
  'type' => 'string',
741
  );
742
  $query_params['orderby'] = array(
743
  'default' => 'name',
744
- 'description' => 'Sort collection by object attribute.',
745
- 'enum' => array( 'id', 'name', 'registered_date' ),
 
 
 
 
 
746
  'sanitize_callback' => 'sanitize_key',
747
  'type' => 'string',
748
  );
10
  */
11
  public function register_routes() {
12
 
 
13
  register_rest_route( 'wp/v2', '/users', array(
14
  array(
15
  'methods' => WP_REST_Server::READABLE,
16
  'callback' => array( $this, 'get_items' ),
17
+ 'args' => $this->get_collection_params(),
 
18
  ),
19
  array(
20
  'methods' => WP_REST_Server::CREATABLE,
21
  'callback' => array( $this, 'create_item' ),
22
  'permission_callback' => array( $this, 'create_item_permissions_check' ),
23
+ 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
24
  'password' => array(
25
  'required' => true,
26
  ),
27
  ) ),
28
  ),
29
+
30
+ 'schema' => array( $this, 'get_public_item_schema' ),
31
  ) );
32
  register_rest_route( 'wp/v2', '/users/(?P<id>[\d]+)', array(
33
  array(
35
  'callback' => array( $this, 'get_item' ),
36
  'permission_callback' => array( $this, 'get_item_permissions_check' ),
37
  'args' => array(
38
+ 'context' => $this->get_context_param( array( 'default' => 'embed' ) ),
 
 
39
  ),
40
  ),
41
  array(
42
  'methods' => WP_REST_Server::EDITABLE,
43
  'callback' => array( $this, 'update_item' ),
44
  'permission_callback' => array( $this, 'update_item_permissions_check' ),
45
+ 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), array(
46
  'password' => array(),
47
  ) ),
48
  ),
51
  'callback' => array( $this, 'delete_item' ),
52
  'permission_callback' => array( $this, 'delete_item_permissions_check' ),
53
  'args' => array(
54
+ 'force' => array(
55
+ 'default' => false,
56
+ ),
57
  'reassign' => array(),
58
  ),
59
  ),
60
+
61
+ 'schema' => array( $this, 'get_public_item_schema' ),
62
  ) );
63
 
64
  register_rest_route( 'wp/v2', '/users/me', array(
67
  'args' => array(
68
  'context' => array(),
69
  ),
70
+ 'schema' => array( $this, 'get_public_item_schema' ),
71
  ));
 
 
 
 
 
72
  }
73
 
74
  /**
80
  public function get_items( $request ) {
81
 
82
  $prepared_args = array();
83
+ $prepared_args['include'] = $request['include'];
84
  $prepared_args['order'] = $request['order'];
85
  $prepared_args['number'] = $request['per_page'];
86
  $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
87
  $orderby_possibles = array(
88
  'id' => 'ID',
89
+ 'include' => 'include',
90
  'name' => 'display_name',
91
  'registered_date' => 'registered',
92
+ );
93
  $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
94
  $prepared_args['search'] = $request['search'];
95
 
96
+ if ( ! current_user_can( 'list_users' ) ) {
97
+ $prepared_args['has_published_posts'] = true;
98
+
99
+ // Only display a public subset of information
100
+ $request['context'] = 'embed';
101
+ }
102
+
103
+ if ( '' !== $prepared_args['search'] ) {
104
+ $prepared_args['search'] = '*' . $prepared_args['search'] . '*';
105
+ }
106
+
107
+ /**
108
+ * Filter arguments, before passing to WP_User_Query, when querying users via the REST API.
109
+ *
110
+ * @see https://codex.wordpress.org/Class_Reference/WP_User_Query
111
+ *
112
+ * @param array $prepared_args Array of arguments for WP_User_Query.
113
+ * @param WP_REST_Request $request The current request.
114
+ */
115
  $prepared_args = apply_filters( 'rest_user_query', $prepared_args, $request );
116
 
117
  $query = new WP_User_Query( $prepared_args );
126
  }
127
 
128
  $response = rest_ensure_response( $users );
129
+
130
+ // Store pagation values for headers then unset for count query.
131
+ $per_page = (int) $prepared_args['number'];
132
+ $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
133
  unset( $prepared_args['number'] );
134
  unset( $prepared_args['offset'] );
135
+
136
+ $prepared_args['fields'] = 'ID';
137
+
138
  $count_query = new WP_User_Query( $prepared_args );
139
  $total_users = $count_query->get_total();
140
  $response->header( 'X-WP-Total', (int) $total_users );
141
+ $max_pages = ceil( $total_users / $per_page );
142
  $response->header( 'X-WP-TotalPages', (int) $max_pages );
143
 
144
  $base = add_query_arg( $request->get_query_params(), rest_url( '/wp/v2/users' ) );
145
+ if ( $page > 1 ) {
146
+ $prev_page = $page - 1;
147
  if ( $prev_page > $max_pages ) {
148
  $prev_page = $max_pages;
149
  }
150
  $prev_link = add_query_arg( 'page', $prev_page, $base );
151
  $response->link_header( 'prev', $prev_link );
152
  }
153
+ if ( $max_pages > $page ) {
154
+ $next_page = $page + 1;
155
  $next_link = add_query_arg( 'page', $next_page, $base );
156
  $response->link_header( 'next', $next_link );
157
  }
170
  $user = get_userdata( $id );
171
 
172
  if ( empty( $id ) || empty( $user->ID ) ) {
173
+ return new WP_Error( 'rest_user_invalid_id', __( 'Invalid user id.' ), array( 'status' => 404 ) );
174
  }
175
 
176
  $user = $this->prepare_item_for_response( $user, $request );
191
  return new WP_Error( 'rest_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) );
192
  }
193
 
194
+ $get_request = new WP_REST_Request;
195
+ $get_request->set_param( 'id', $current_user_id );
196
+ $get_request->set_param( 'context', $request['context'] );
197
+ $response = $this->get_item( $get_request );
198
  if ( is_wp_error( $response ) ) {
199
  return $response;
200
  }
219
  return new WP_Error( 'rest_user_exists', __( 'Cannot create existing user.' ), array( 'status' => 400 ) );
220
  }
221
 
 
 
 
 
222
  $user = $this->prepare_item_for_database( $request );
223
 
224
  if ( is_multisite() ) {
248
 
249
  $this->update_additional_fields_for_object( $user, $request );
250
 
251
+ /**
252
+ * Fires after a user is created or updated via the REST API.
253
+ *
254
+ * @param object $user Data used to create the user (not a WP_User object).
255
+ * @param WP_REST_Request $request Request object.
256
+ * @param bool $creating True when creating user, false when updating user.
257
+ */
258
+ do_action( 'rest_insert_user', $user, $request, true );
259
+
260
+ $get_request = new WP_REST_Request;
261
+ $get_request->set_param( 'id', $user_id );
262
+ $get_request->set_param( 'context', 'edit' );
263
+ $response = $this->get_item( $get_request );
264
  $response = rest_ensure_response( $response );
265
  $response->set_status( 201 );
266
  $response->header( 'Location', rest_url( '/wp/v2/users/' . $user_id ) );
279
 
280
  $user = get_userdata( $id );
281
  if ( ! $user ) {
282
+ return new WP_Error( 'rest_user_invalid_id', __( 'User id is invalid.' ), array( 'status' => 400 ) );
283
  }
284
 
285
  if ( email_exists( $request['email'] ) && $request['email'] !== $user->user_email ) {
313
 
314
  $this->update_additional_fields_for_object( $user, $request );
315
 
316
+ /* This action is documented in lib/endpoints/class-wp-rest-users-controller.php */
317
  do_action( 'rest_insert_user', $user, $request, false );
318
 
319
+ $get_request = new WP_REST_Request;
320
+ $get_request->set_param( 'id', $user_id );
321
+ $get_request->set_param( 'context', 'edit' );
322
+ $response = $this->get_item( $get_request );
 
 
323
 
324
+ return rest_ensure_response( $response );
325
  }
326
 
327
  /**
337
 
338
  // We don't support trashing for this type, error out
339
  if ( ! $force ) {
340
+ return new WP_Error( 'rest_trash_not_supported', __( 'Users do not support trashing.' ), array( 'status' => 501 ) );
341
  }
342
 
343
  $user = get_userdata( $id );
344
  if ( ! $user ) {
345
+ return new WP_Error( 'rest_user_invalid_id', __( 'Invalid user id.' ), array( 'status' => 400 ) );
346
  }
347
 
348
  if ( ! empty( $reassign ) ) {
349
  if ( $reassign === $id || ! get_userdata( $reassign ) ) {
350
+ return new WP_Error( 'rest_user_invalid_reassign', __( 'Invalid user id.' ), array( 'status' => 400 ) );
351
  }
352
  }
353
 
354
+ $get_request = new WP_REST_Request;
355
+ $get_request->set_param( 'id', $id );
356
  $get_request->set_param( 'context', 'edit' );
357
  $orig_user = $this->prepare_item_for_response( $user, $get_request );
358
 
359
+ $data = $orig_user->get_data();
360
+ $data = array(
361
+ 'data' => $data,
362
+ 'deleted' => true,
363
+ );
364
+ $orig_user->set_data( $data );
365
+
366
+ /** Include admin user functions to get access to wp_delete_user() */
367
+ require_once ABSPATH . 'wp-admin/includes/user.php';
368
+
369
  $result = wp_delete_user( $id, $reassign );
370
 
371
  if ( ! $result ) {
372
  return new WP_Error( 'rest_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 500 ) );
373
  }
374
 
375
+ /**
376
+ * Fires after a user is deleted via the REST API.
377
+ *
378
+ * @param WP_User $user The user data.
379
+ * @param WP_REST_Request $request The request sent to the API.
380
+ */
381
+ do_action( 'rest_delete_user', $user, $data, $request );
 
 
 
382
 
383
+ return $orig_user;
 
 
 
 
384
  }
385
 
386
  /**
395
  $user = get_userdata( $id );
396
 
397
  if ( empty( $id ) || empty( $user->ID ) ) {
398
+ return new WP_Error( 'rest_user_invalid_id', __( 'Invalid user id.' ), array( 'status' => 404 ) );
399
  }
400
 
401
  if ( get_current_user_id() === $id ) {
405
  $context = ! empty( $request['context'] ) && in_array( $request['context'], array( 'edit', 'view', 'embed' ) ) ? $request['context'] : 'embed';
406
 
407
  if ( 'edit' === $context && ! current_user_can( 'edit_user', $id ) ) {
408
+ return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this user with edit context' ), array( 'status' => rest_authorization_required_code() ) );
409
  } else if ( 'view' === $context && ! current_user_can( 'list_users' ) ) {
410
+ return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this user with view context' ), array( 'status' => rest_authorization_required_code() ) );
411
  } else if ( 'embed' === $context && ! count_user_posts( $id ) && ! current_user_can( 'edit_user', $id ) && ! current_user_can( 'list_users' ) ) {
412
+ return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this user' ), array( 'status' => rest_authorization_required_code() ) );
413
  }
414
 
415
  return true;
424
  public function create_item_permissions_check( $request ) {
425
 
426
  if ( ! current_user_can( 'create_users' ) ) {
427
+ return new WP_Error( 'rest_cannot_create_user', __( 'Sorry, you are not allowed to create users.' ), array( 'status' => rest_authorization_required_code() ) );
428
  }
429
 
430
  return true;
441
  $id = (int) $request['id'];
442
 
443
  if ( ! current_user_can( 'edit_user', $id ) ) {
444
+ return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit users.' ), array( 'status' => rest_authorization_required_code() ) );
445
  }
446
 
447
  if ( ! empty( $request['role'] ) && ! current_user_can( 'edit_users' ) ) {
448
+ return new WP_Error( 'rest_cannot_edit_roles', __( 'Sorry, you are not allowed to edit roles of users.' ), array( 'status' => rest_authorization_required_code() ) );
449
  }
450
 
451
  return true;
463
  $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null;
464
 
465
  if ( ! current_user_can( 'delete_user', $id ) ) {
466
+ return new WP_Error( 'rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this user.' ), array( 'status' => rest_authorization_required_code() ) );
467
  }
468
 
469
  return true;
474
  *
475
  * @param object $user User object.
476
  * @param WP_REST_Request $request Request object.
477
+ * @return WP_REST_Response Response data.
478
  */
479
  public function prepare_item_for_response( $user, $request ) {
480
  $data = array(
 
 
 
 
 
 
481
  'id' => $user->ID,
482
+ 'username' => $user->user_login,
483
+ 'name' => $user->display_name,
484
+ 'first_name' => $user->first_name,
485
  'last_name' => $user->last_name,
486
+ 'email' => $user->user_email,
487
+ 'url' => $user->user_url,
488
+ 'description' => $user->description,
489
  'link' => get_author_posts_url( $user->ID ),
490
+ 'avatar_urls' => rest_get_avatar_urls( $user->user_email ),
491
  'nickname' => $user->nickname,
492
+ 'slug' => $user->user_nicename,
493
  'registered_date' => date( 'c', strtotime( $user->user_registered ) ),
494
  'roles' => $user->roles,
495
+ 'capabilities' => $user->allcaps,
496
+ 'extra_capabilities' => $user->caps,
 
497
  );
498
 
499
  $context = ! empty( $request['context'] ) ? $request['context'] : 'embed';
502
  $data = $this->add_additional_fields_to_object( $data, $request );
503
 
504
  // Wrap the data in a response object
505
+ $response = rest_ensure_response( $data );
506
+
507
+ $response->add_links( $this->prepare_links( $user ) );
508
+
509
+ /**
510
+ * Filter user data returned from the REST API.
511
+ *
512
+ * @param WP_REST_Response $response The response object.
513
+ * @param object $user User object used to create response.
514
+ * @param WP_REST_Request $request Request object.
515
+ */
516
+ return apply_filters( 'rest_prepare_user', $response, $user, $request );
517
  }
518
 
519
  /**
578
  $prepared_user->description = $request['description'];
579
  }
580
  if ( isset( $request['role'] ) ) {
581
+ $prepared_user->role = $request['role'];
582
  }
583
  if ( isset( $request['url'] ) ) {
584
  $prepared_user->user_url = $request['url'];
585
  }
586
 
587
+ /**
588
+ * Filter user data before inserting user via the REST API.
589
+ *
590
+ * @param object $prepared_user User object.
591
+ * @param WP_REST_Request $request Request object.
592
+ */
593
  return apply_filters( 'rest_pre_insert_user', $prepared_user, $request );
594
  }
595
 
603
  protected function check_role_update( $user_id, $role ) {
604
  global $wp_roles;
605
 
 
 
 
 
606
  $potential_role = $wp_roles->role_objects[ $role ];
607
 
608
  // Don't let anyone with 'edit_users' (admins) edit their own role to something without it.
609
  // Multisite super admins can freely edit their blog roles -- they possess all caps.
610
  if ( ( is_multisite() && current_user_can( 'manage_sites' ) ) || get_current_user_id() !== $user_id || $potential_role->has_cap( 'edit_users' ) ) {
611
  // The new role must be editable by the logged-in user.
612
+
613
+ /** Include admin functions to get access to get_editable_roles() */
614
+ require_once ABSPATH . 'wp-admin/includes/admin.php';
615
+
616
  $editable_roles = get_editable_roles();
617
  if ( empty( $editable_roles[ $role ] ) ) {
618
  return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give users that role.' ), array( 'status' => 403 ) );
621
  return true;
622
  }
623
 
624
+ return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give users that role.' ), array( 'status' => rest_authorization_required_code() ) );
625
  }
626
 
627
  /**
635
  $avatar_sizes = rest_get_avatar_sizes();
636
  foreach ( $avatar_sizes as $size ) {
637
  $avatar_properties[ $size ] = array(
638
+ 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
639
+ 'type' => 'string',
640
+ 'format' => 'uri',
641
  'context' => array( 'embed', 'view', 'edit' ),
642
  );
643
  }
644
 
645
+ global $wp_roles;
646
+
647
  $schema = array(
648
  '$schema' => 'http://json-schema.org/draft-04/schema#',
649
  'title' => 'user',
650
  'type' => 'object',
651
  'properties' => array(
652
+ 'id' => array(
653
+ 'description' => __( 'Unique identifier for the object.' ),
654
+ 'type' => 'integer',
655
  'context' => array( 'embed', 'view', 'edit' ),
656
  'readonly' => true,
 
657
  ),
658
+ 'username' => array(
659
+ 'description' => __( 'Login name for the user.' ),
 
 
 
 
 
660
  'type' => 'string',
661
+ 'context' => array( 'edit' ),
662
+ 'required' => true,
663
  'arg_options' => array(
664
+ 'sanitize_callback' => 'sanitize_user',
665
  ),
666
  ),
667
+ 'name' => array(
668
+ 'description' => __( 'Display name for the object.' ),
669
  'type' => 'string',
670
+ 'context' => array( 'embed', 'view', 'edit' ),
671
+ 'arg_options' => array(
672
+ 'sanitize_callback' => 'sanitize_text_field',
 
 
 
 
 
 
673
  ),
674
+ ),
675
  'first_name' => array(
676
+ 'description' => __( 'First name for the object.' ),
677
  'type' => 'string',
678
  'context' => array( 'view', 'edit' ),
679
  'arg_options' => array(
680
  'sanitize_callback' => 'sanitize_text_field',
681
  ),
682
  ),
 
 
 
 
 
 
683
  'last_name' => array(
684
+ 'description' => __( 'Last name for the object.' ),
685
  'type' => 'string',
686
  'context' => array( 'view', 'edit' ),
687
  'arg_options' => array(
688
  'sanitize_callback' => 'sanitize_text_field',
689
  ),
690
  ),
691
+ 'email' => array(
692
+ 'description' => __( 'The email address for the object.' ),
693
+ 'type' => 'string',
694
+ 'format' => 'email',
695
+ 'context' => array( 'view', 'edit' ),
696
+ 'required' => true,
697
+ ),
698
+ 'url' => array(
699
+ 'description' => __( 'URL of the object.' ),
700
  'type' => 'string',
701
  'format' => 'uri',
702
  'context' => array( 'embed', 'view', 'edit' ),
703
  'readonly' => true,
704
  ),
705
+ 'description' => array(
706
+ 'description' => __( 'Description of the object.' ),
707
  'type' => 'string',
708
  'context' => array( 'embed', 'view', 'edit' ),
709
  'arg_options' => array(
710
+ 'sanitize_callback' => 'wp_filter_post_kses',
711
  ),
712
  ),
713
+ 'link' => array(
714
+ 'description' => __( 'Author URL to the object.' ),
715
+ 'type' => 'string',
716
+ 'format' => 'uri',
717
+ 'context' => array( 'embed', 'view', 'edit' ),
718
+ 'readonly' => true,
719
+ ),
720
+ 'avatar_urls' => array(
721
+ 'description' => __( 'Avatar URLs for the object.' ),
722
+ 'type' => 'object',
723
+ 'context' => array( 'embed', 'view', 'edit' ),
724
+ 'readonly' => true,
725
+ 'properties' => $avatar_properties,
726
+ ),
727
  'nickname' => array(
728
+ 'description' => __( 'The nickname for the object.' ),
729
  'type' => 'string',
730
  'context' => array( 'view', 'edit' ),
731
  'arg_options' => array(
732
  'sanitize_callback' => 'sanitize_text_field',
733
  ),
734
  ),
735
+ 'slug' => array(
736
+ 'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
737
+ 'type' => 'string',
738
+ 'context' => array( 'embed', 'view', 'edit' ),
739
+ 'arg_options' => array(
740
+ 'sanitize_callback' => 'sanitize_title',
741
+ ),
742
+ ),
743
  'registered_date' => array(
744
+ 'description' => __( 'Registration date for the user.' ),
745
  'type' => 'date-time',
746
  'context' => array( 'view', 'edit' ),
747
  'readonly' => true,
748
  ),
749
  'roles' => array(
750
+ 'description' => __( 'Roles assigned to the user.' ),
751
  'type' => 'array',
752
  'context' => array( 'view', 'edit' ),
753
+ 'readonly' => true,
754
  ),
755
+ 'role' => array(
756
+ 'description' => __( 'Role assigned to the user.' ),
757
  'type' => 'string',
758
+ 'enum' => array_keys( $wp_roles->role_objects ),
 
 
 
759
  ),
760
+ 'capabilities' => array(
761
+ 'description' => __( 'All capabilities assigned to the user.' ),
762
+ 'type' => 'object',
763
+ 'context' => array( 'view', 'edit' ),
 
 
764
  ),
765
+ 'extra_capabilities' => array(
766
+ 'description' => __( 'Any extra capabilities assigned to the user.' ),
767
+ 'type' => 'object',
768
  'context' => array( 'edit' ),
769
+ 'readonly' => true,
 
 
 
770
  ),
771
  ),
772
  );
780
  */
781
  public function get_collection_params() {
782
  $query_params = parent::get_collection_params();
783
+
784
+ $query_params['context']['default'] = 'view';
785
+
786
+ $query_params['include'] = array(
787
+ 'description' => __( 'Limit result set to specific ids.' ),
788
+ 'type' => 'array',
789
+ 'default' => array(),
790
+ 'sanitize_callback' => 'wp_parse_id_list',
791
  );
792
  $query_params['order'] = array(
793
  'default' => 'asc',
794
+ 'description' => __( 'Order sort attribute ascending or descending.' ),
795
  'enum' => array( 'asc', 'desc' ),
796
  'sanitize_callback' => 'sanitize_key',
797
  'type' => 'string',
798
  );
799
  $query_params['orderby'] = array(
800
  'default' => 'name',
801
+ 'description' => __( 'Sort collection by object attribute.' ),
802
+ 'enum' => array(
803
+ 'id',
804
+ 'include',
805
+ 'name',
806
+ 'registered_date',
807
+ ),
808
  'sanitize_callback' => 'sanitize_key',
809
  'type' => 'string',
810
  );
lib/infrastructure/class-jsonserializable.php DELETED
@@ -1,18 +0,0 @@
1
- <?php
2
- /**
3
- * Compatibility shim for PHP <5.4
4
- *
5
- * @link http://php.net/jsonserializable
6
- *
7
- * @package WordPress
8
- * @subpackage JSON API
9
- */
10
-
11
- if ( ! interface_exists( 'JsonSerializable' ) ) {
12
- define( 'WP_JSON_SERIALIZE_COMPATIBLE', true );
13
- // @codingStandardsIgnoreStart
14
- interface JsonSerializable {
15
- public function jsonSerialize();
16
- }
17
- // @codingStandardsIgnoreEnd
18
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lib/infrastructure/class-wp-http-response.php DELETED
@@ -1,112 +0,0 @@
1
- <?php
2
-
3
- class WP_HTTP_Response implements WP_HTTP_ResponseInterface {
4
- /**
5
- * @var mixed
6
- */
7
- public $data;
8
- /**
9
- * @var integer
10
- */
11
- public $headers;
12
- /**
13
- * @var array
14
- */
15
- public $status;
16
- /**
17
- * Constructor
18
- *
19
- * @param mixed $data Response data
20
- * @param integer $status HTTP status code
21
- * @param array $headers HTTP header map
22
- */
23
- public function __construct( $data = null, $status = 200, $headers = array() ) {
24
- $this->data = $data;
25
- $this->set_status( $status );
26
- $this->set_headers( $headers );
27
- }
28
-
29
- /**
30
- * Get headers associated with the response
31
- *
32
- * @return array Map of header name to header value
33
- */
34
- public function get_headers() {
35
- return $this->headers;
36
- }
37
-
38
- /**
39
- * Set all header values
40
- *
41
- * @param array $headers Map of header name to header value
42
- */
43
- public function set_headers( $headers ) {
44
- $this->headers = $headers;
45
- }
46
-
47
- /**
48
- * Set a single HTTP header
49
- *
50
- * @param string $key Header name
51
- * @param string $value Header value
52
- * @param boolean $replace Replace an existing header of the same name?
53
- */
54
- public function header( $key, $value, $replace = true ) {
55
- if ( $replace || ! isset( $this->headers[ $key ] ) ) {
56
- $this->headers[ $key ] = $value;
57
- } else {
58
- $this->headers[ $key ] .= ', ' . $value;
59
- }
60
- }
61
-
62
- /**
63
- * Get the HTTP return code for the response
64
- *
65
- * @return integer 3-digit HTTP status code
66
- */
67
- public function get_status() {
68
- return $this->status;
69
- }
70
-
71
- /**
72
- * Set the HTTP status code
73
- *
74
- * @param int $code HTTP status
75
- */
76
- public function set_status( $code ) {
77
- $this->status = absint( $code );
78
- }
79
-
80
- /**
81
- * Get the response data
82
- *
83
- * @return mixed
84
- */
85
- public function get_data() {
86
- return $this->data;
87
- }
88
-
89
- /**
90
- * Set the response data
91
- *
92
- * @param mixed $data
93
- */
94
- public function set_data( $data ) {
95
- $this->data = $data;
96
- }
97
-
98
- /**
99
- * Get the response data for JSON serialization
100
- *
101
- * It is expected that in most implementations, this will return the same as
102
- * {@see get_data()}, however this may be different if you want to do custom
103
- * JSON data handling.
104
- *
105
- * @return mixed Any JSON-serializable value
106
- */
107
- // @codingStandardsIgnoreStart
108
- public function jsonSerialize() {
109
- // @codingStandardsIgnoreEnd
110
- return $this->get_data();
111
- }
112
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lib/infrastructure/class-wp-http-responseinterface.php DELETED
@@ -1,35 +0,0 @@
1
- <?php
2
-
3
- interface WP_HTTP_ResponseInterface extends JsonSerializable {
4
- /**
5
- * Get headers associated with the response
6
- *
7
- * @return array Map of header name to header value
8
- */
9
- public function get_headers();
10
-
11
- /**
12
- * Get the HTTP return code for the response
13
- *
14
- * @return integer 3-digit HTTP status code
15
- */
16
- public function get_status();
17
-
18
- /**
19
- * Get the response data
20
- *
21
- * @return mixed
22
- */
23
- public function get_data();
24
-
25
- /**
26
- * Get the response data for JSON serialization
27
- *
28
- * It is expected that in most implementations, this will return the same as
29
- * {@see get_data()}, however this may be different if you want to do custom
30
- * JSON data handling.
31
- *
32
- * @return mixed Any JSON-serializable value
33
- */
34
- // public function jsonSerialize();
35
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lib/infrastructure/class-wp-rest-request.php DELETED
@@ -1,764 +0,0 @@
1
- <?php
2
-
3
- /**
4
- * Request object
5
- *
6
- * Contains data from the request, to be passed to the callback.
7
- *
8
- * Note: This implements ArrayAccess, and acts as an array of parameters when
9
- * used in that manner. It does not use ArrayObject (as we cannot rely on SPL),
10
- * so be aware it may have non-array behaviour in some cases.
11
- *
12
- * @package WordPress
13
- */
14
- class WP_REST_Request implements ArrayAccess {
15
- /**
16
- * HTTP method
17
- *
18
- * @var string
19
- */
20
- protected $method = '';
21
-
22
- /**
23
- * Parameters passed to the request
24
- *
25
- * These typically come from the `$_GET`, `$_POST` and `$_FILES`
26
- * superglobals when being created from the global scope.
27
- *
28
- * @var array Contains GET, POST and FILES keys mapping to arrays of data
29
- */
30
- protected $params;
31
-
32
- /**
33
- * HTTP headers for the request
34
- *
35
- * @var array Map of key to value. Key is always lowercase, as per HTTP specification
36
- */
37
- protected $headers = array();
38
-
39
- /**
40
- * Body data
41
- *
42
- * @var string Binary data from the request
43
- */
44
- protected $body = null;
45
-
46
- /**
47
- * Route matched for the request
48
- *
49
- * @var string
50
- */
51
- protected $route;
52
-
53
- /**
54
- * Attributes (options) for the route that was matched
55
- *
56
- * This is the options array used when the route was registered, typically
57
- * containing the callback as well as the valid methods for the route.
58
- *
59
- * @return array Attributes for the request
60
- */
61
- protected $attributes = array();
62
-
63
- /**
64
- * Have we parsed the JSON data yet?
65
- *
66
- * Allows lazy-parsing of JSON data where possible.
67
- *
68
- * @var boolean
69
- */
70
- protected $parsed_json = false;
71
-
72
- /**
73
- * Have we parsed body data yet?
74
- *
75
- * @var boolean
76
- */
77
- protected $parsed_body = false;
78
-
79
- /**
80
- * Constructor
81
- */
82
- public function __construct( $method = '', $route = '', $attributes = array() ) {
83
- $this->params = array(
84
- 'URL' => array(),
85
- 'GET' => array(),
86
- 'POST' => array(),
87
- 'FILES' => array(),
88
-
89
- // See parse_json_params
90
- 'JSON' => null,
91
-
92
- 'defaults' => array(),
93
- );
94
-
95
- $this->set_method( $method );
96
- $this->set_route( $route );
97
- $this->set_attributes( $attributes );
98
- }
99
-
100
- /**
101
- * Get HTTP method for the request
102
- *
103
- * @return string HTTP method
104
- */
105
- public function get_method() {
106
- return $this->method;
107
- }
108
-
109
- /**
110
- * Set HTTP method for the request
111
- *
112
- * @param string $method HTTP method
113
- */
114
- public function set_method( $method ) {
115
- $this->method = strtoupper( $method );
116
- }
117
-
118
- /**
119
- * Get all headers from the request
120
- *
121
- * @return array Map of key to value. Key is always lowercase, as per HTTP specification
122
- */
123
- public function get_headers() {
124
- return $this->headers;
125
- }
126
-
127
- /**
128
- * Canonicalize header name
129
- *
130
- * Ensures that header names are always treated the same regardless of
131
- * source. Header names are always case insensitive.
132
- *
133
- * Note that we treat `-` (dashes) and `_` (underscores) as the same
134
- * character, as per header parsing rules in both Apache and nginx.
135
- *
136
- * @link http://stackoverflow.com/q/18185366
137
- * @link http://wiki.nginx.org/Pitfalls#Missing_.28disappearing.29_HTTP_headers
138
- * @link http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
139
- *
140
- * @param string $key Header name
141
- * @return string Canonicalized name
142
- */
143
- public static function canonicalize_header_name( $key ) {
144
- $key = strtolower( $key );
145
- $key = str_replace( '-', '_', $key );
146
-
147
- return $key;
148
- }
149
-
150
- /**
151
- * Get header from request
152
- *
153
- * If the header has multiple values, they will be concatenated with a comma
154
- * as per the HTTP specification. Be aware that some non-compliant headers
155
- * (notably cookie headers) cannot be joined this way.
156
- *
157
- * @param string $key Header name, will be canonicalized to lowercase
158
- * @return string|null String value if set, null otherwise
159
- */
160
- public function get_header( $key ) {
161
- $key = $this->canonicalize_header_name( $key );
162
-
163
- if ( ! isset( $this->headers[ $key ] ) ) {
164
- return null;
165
- }
166
-
167
- return implode( ',', $this->headers[ $key ] );
168
- }
169
-
170
- /**
171
- * Get header values from request
172
- *
173
- * @param string $key Header name, will be canonicalized to lowercase
174
- * @return array|null List of string values if set, null otherwise
175
- */
176
- public function get_header_as_array( $key ) {
177
- $key = $this->canonicalize_header_name( $key );
178
-
179
- if ( ! isset( $this->headers[ $key ] ) ) {
180
- return null;
181
- }
182
-
183
- return $this->headers[ $key ];
184
- }
185
-
186
- /**
187
- * Set header on request
188
- *
189
- * @param string $key Header name
190
- * @param string|string[] $value Header value, or list of values
191
- */
192
- public function set_header( $key, $value ) {
193
- $key = $this->canonicalize_header_name( $key );
194
- $value = (array) $value;
195
-
196
- $this->headers[ $key ] = $value;
197
- }
198
-
199
- /**
200
- * Append a header value for the given header
201
- *
202
- * @param string $key Header name
203
- * @param string|string[] $value Header value, or list of values
204
- */
205
- public function add_header( $key, $value ) {
206
- $key = $this->canonicalize_header_name( $key );
207
- $value = (array) $value;
208
-
209
- if ( ! isset( $this->headers[ $key ] ) ) {
210
- $this->headers[ $key ] = array();
211
- }
212
-
213
- $this->headers[ $key ] = array_merge( $this->headers[ $key ], $value );
214
- }
215
-
216
- /**
217
- * Remove all values for a header
218
- *
219
- * @param string $key Header name
220
- */
221
- public function remove_header( $key ) {
222
- unset( $this->headers[ $key ] );
223
- }
224
-
225
- /**
226
- * Set headers on the request
227
- *
228
- * @param array $headers Map of header name to value
229
- * @param boolean $override If true, replace the request's headers. Otherwise, merge with existing.
230
- */
231
- public function set_headers( $headers, $override = true ) {
232
- if ( true === $override ) {
233
- $this->headers = array();
234
- }
235
-
236
- foreach ( $headers as $key => $value ) {
237
- $this->set_header( $key, $value );
238
- }
239
- }
240
-
241
- /**
242
- * Get the content-type of the request
243
- *
244
- * @return array Map containing 'value' and 'parameters' keys
245
- */
246
- public function get_content_type() {
247
- $value = $this->get_header( 'content-type' );
248
- if ( empty( $value ) ) {
249
- return null;
250
- }
251
-
252
- $parameters = '';
253
- if ( strpos( $value, ';' ) ) {
254
- list( $value, $parameters ) = explode( ';', $value, 2 );
255
- }
256
-
257
- $value = strtolower( $value );
258
- if ( strpos( $value, '/' ) === false ) {
259
- return null;
260
- }
261
-
262
- // Parse type and subtype out
263
- list( $type, $subtype ) = explode( '/', $value, 2 );
264
-
265
- $data = compact( 'value', 'type', 'subtype', 'parameters' );
266
- $data = array_map( 'trim', $data );
267
-
268
- return $data;
269
- }
270
-
271
- /**
272
- * Get the parameter priority order
273
- *
274
- * Used when checking parameters in {@see get_param}.
275
- *
276
- * @return string[] List of types to check, in order of priority
277
- */
278
- protected function get_parameter_order() {
279
- $order = array();
280
- $order[] = 'JSON';
281
-
282
- $this->parse_json_params();
283
-
284
- // Ensure we parse the body data
285
- $body = $this->get_body();
286
- if ( $this->method !== 'POST' && ! empty( $body ) ) {
287
- $this->parse_body_params();
288
- }
289
-
290
- $accepts_body_data = array( 'POST', 'PUT', 'PATCH' );
291
- if ( in_array( $this->method, $accepts_body_data ) ) {
292
- $order[] = 'POST';
293
- }
294
-
295
- $order[] = 'GET';
296
- $order[] = 'URL';
297
- $order[] = 'defaults';
298
-
299
- /**
300
- * Alter the parameter checking order
301
- *
302
- * The order affects which parameters are checked when using
303
- * {@see get_param} and family. This acts similarly to PHP's
304
- * `request_order` setting.
305
- *
306
- * @param string[] $order List of types to check, in order of priority
307
- * @param WP_REST_Request $this Request object
308
- */
309
- return apply_filters( 'rest_request_parameter_order', $order, $this );
310
- }
311
-
312
- /**
313
- * Get a parameter from the request
314
- *
315
- * @param string $key Parameter name
316
- * @return mixed|null Value if set, null otherwise
317
- */
318
- public function get_param( $key ) {
319
- $order = $this->get_parameter_order();
320
-
321
- foreach ( $order as $type ) {
322
- // Do we have the parameter for this type?
323
- if ( isset( $this->params[ $type ][ $key ] ) ) {
324
- return $this->params[ $type ][ $key ];
325
- }
326
- }
327
-
328
- return null;
329
- }
330
-
331
- /**
332
- * Set a parameter on the request
333
- *
334
- * @param string $key Parameter name
335
- * @param mixed $value Parameter value
336
- */
337
- public function set_param( $key, $value ) {
338
- switch ( $this->method ) {
339
- case 'POST':
340
- $this->params['POST'][ $key ] = $value;
341
- break;
342
-
343
- default:
344
- $this->params['GET'][ $key ] = $value;
345
- break;
346
- }
347
- }
348
-
349
- /**
350
- * Get merged parameters from the request
351
- *
352
- * The equivalent of {@see get_param}, but returns all parameters for the
353
- * request. Handles merging all the available values into a single array.
354
- *
355
- * @return array Map of key to value
356
- */
357
- public function get_params() {
358
- $order = $this->get_parameter_order();
359
- $order = array_reverse( $order, true );
360
-
361
- $params = array();
362
- foreach ( $order as $type ) {
363
- $params = array_merge( $params, (array) $this->params[ $type ] );
364
- }
365
-
366
- return $params;
367
- }
368
-
369
- /**
370
- * Get parameters from the route itself
371
- *
372
- * These are parsed from the URL using the regex.
373
- *
374
- * @return array Parameter map of key to value
375
- */
376
- public function get_url_params() {
377
- return $this->params['URL'];
378
- }
379
-
380
- /**
381
- * Set parameters from the route
382
- *
383
- * Typically, this is set after parsing the URL.
384
- *
385
- * @param array $params Parameter map of key to value
386
- */
387
- public function set_url_params( $params ) {
388
- $this->params['URL'] = $params;
389
- }
390
-
391
- /**
392
- * Get parameters from the query string
393
- *
394
- * These are the parameters you'd typically find in `$_GET`
395
- *
396
- * @return array Parameter map of key to value
397
- */
398
- public function get_query_params() {
399
- return $this->params['GET'];
400
- }
401
-
402
- /**
403
- * Set parameters from the query string
404
- *
405
- * Typically, this is set from `$_GET`
406
- *
407
- * @param array $params Parameter map of key to value
408
- */
409
- public function set_query_params( $params ) {
410
- $this->params['GET'] = $params;
411
- }
412
-
413
- /**
414
- * Get parameters from the body
415
- *
416
- * These are the parameters you'd typically find in `$_POST`
417
- *
418
- * @return array Parameter map of key to value
419
- */
420
- public function get_body_params() {
421
- return $this->params['POST'];
422
- }
423
-
424
- /**
425
- * Set parameters from the body
426
- *
427
- * Typically, this is set from `$_POST`
428
- *
429
- * @param array $params Parameter map of key to value
430
- */
431
- public function set_body_params( $params ) {
432
- $this->params['POST'] = $params;
433
- }
434
-
435
- /**
436
- * Get multipart file parameters from the body
437
- *
438
- * These are the parameters you'd typically find in `$_FILES`
439
- *
440
- * @return array Parameter map of key to value
441
- */
442
- public function get_file_params() {
443
- return $this->params['FILES'];
444
- }
445
-
446
- /**
447
- * Set multipart file parameters from the body
448
- *
449
- * Typically, this is set from `$_FILES`
450
- *
451
- * @param array $params Parameter map of key to value
452
- */
453
- public function set_file_params( $params ) {
454
- $this->params['FILES'] = $params;
455
- }
456
-
457
- /**
458
- * Get default parameters
459
- *
460
- * These are the parameters set in the route registration
461
- *
462
- * @return array Parameter map of key to value
463
- */
464
- public function get_default_params() {
465
- return $this->params['defaults'];
466
- }
467
-
468
- /**
469
- * Set default parameters
470
- *
471
- * These are the parameters set in the route registration
472
- *
473
- * @param array $params Parameter map of key to value
474
- */
475
- public function set_default_params( $params ) {
476
- $this->params['defaults'] = $params;
477
- }
478
-
479
- /**
480
- * Get body content
481
- *
482
- * @return string Binary data from the request body
483
- */
484
- public function get_body() {
485
- return $this->body;
486
- }
487
-
488
- /**
489
- * Set body content
490
- *
491
- * @param string $data Binary data from the request body
492
- */
493
- public function set_body( $data ) {
494
- $this->body = $data;
495
-
496
- // Enable lazy parsing
497
- $this->parsed_json = false;
498
- $this->parsed_body = false;
499
- $this->params['JSON'] = null;
500
- }
501
-
502
- /**
503
- * Get parameters from a JSON-formatted body
504
- *
505
- * @return array Parameter map of key to value
506
- */
507
- public function get_json_params() {
508
- // Ensure the parameters have been parsed out
509
- $this->parse_json_params();
510
-
511
- return $this->params['JSON'];
512
- }
513
-
514
- /**
515
- * Parse the JSON parameters
516
- *
517
- * Avoids parsing the JSON data until we need to access it.
518
- */
519
- protected function parse_json_params() {
520
- if ( $this->parsed_json ) {
521
- return;
522
- }
523
- $this->parsed_json = true;
524
-
525
- // Check that we actually got JSON
526
- $content_type = $this->get_content_type();
527
- if ( empty( $content_type ) || 'application/json' !== $content_type['value'] ) {
528
- return;
529
- }
530
-
531
- $params = json_decode( $this->get_body(), true );
532
-
533
- // Check for a parsing error
534
- //
535
- // Note that due to WP's JSON compatibility functions, json_last_error
536
- // might not be defined: https://core.trac.wordpress.org/ticket/27799
537
- if ( null === $params && ( ! function_exists( 'json_last_error' ) || JSON_ERROR_NONE !== json_last_error() ) ) {
538
- return;
539
- }
540
-
541
- $this->params['JSON'] = $params;
542
- }
543
-
544
- /**
545
- * Parse body parameters.
546
- *
547
- * Parses out URL-encoded bodies for request methods that aren't supported
548
- * natively by PHP. In PHP 5.x, only POST has these parsed automatically.
549
- */
550
- protected function parse_body_params() {
551
- if ( $this->parsed_body ) {
552
- return;
553
- }
554
- $this->parsed_body = true;
555
-
556
- // Check that we got URL-encoded. Treat a missing content-type as
557
- // URL-encoded for maximum compatibility
558
- $content_type = $this->get_content_type();
559
- if ( ! empty( $content_type ) && 'application/x-www-form-urlencoded' !== $content_type['value'] ) {
560
- return;
561
- }
562
-
563
- parse_str( $this->get_body(), $params );
564
-
565
- // Amazingly, parse_str follows magic quote rules. Sigh.
566
- // NOTE: Do not refactor to use `wp_unslash`.
567
- // @codeCoverageIgnoreStart
568
- if ( get_magic_quotes_gpc() ) {
569
- $params = stripslashes_deep( $params );
570
- }
571
- // @codeCoverageIgnoreEnd
572
-
573
- // Add to the POST parameters stored internally. If a user has already
574
- // set these manually (via `set_body_params`), don't override them.
575
- $this->params['POST'] = array_merge( $params, $this->params['POST'] );
576
- }
577
-
578
- /**
579
- * Get route that matched the request
580
- *
581
- * @return string Route matching regex
582
- */
583
- public function get_route() {
584
- return $this->route;
585
- }
586
-
587
- /**
588
- * Set route that matched the request
589
- *
590
- * @param string $route Route matching regex
591
- */
592
- public function set_route( $route ) {
593
- $this->route = $route;
594
- }
595
-
596
- /**
597
- * Get attributes for the request
598
- *
599
- * These are the options for the route that was matched.
600
- *
601
- * @return array Attributes for the request
602
- */
603
- public function get_attributes() {
604
- return $this->attributes;
605
- }
606
-
607
- /**
608
- * Set attributes for the request
609
- *
610
- * @param array $attributes Attributes for the request
611
- */
612
- public function set_attributes( $attributes ) {
613
- $this->attributes = $attributes;
614
- }
615
-
616
- /**
617
- * Sanitize (where possible) the params on the request.
618
- *
619
- * This is primarily based off the sanitize_callback param on each registered
620
- * argument.
621
- *
622
- * @return null
623
- */
624
- public function sanitize_params() {
625
-
626
- $attributes = $this->get_attributes();
627
-
628
- // No arguments set, skip sanitizing
629
- if ( empty( $attributes['args'] ) ) {
630
- return true;
631
- }
632
-
633
- $order = $this->get_parameter_order();
634
-
635
- foreach ( $order as $type ) {
636
- if ( empty( $this->params[ $type ] ) ) {
637
- continue;
638
- }
639
- foreach ( $this->params[ $type ] as $key => $value ) {
640
- // check if this param has a sanitize_callback added
641
- if ( isset( $attributes['args'][ $key ] ) && ! empty( $attributes['args'][ $key ]['sanitize_callback'] ) ) {
642
- $this->params[ $type ][ $key ] = call_user_func( $attributes['args'][ $key ]['sanitize_callback'], $value, $this, $key );
643
- }
644
- }
645
- }
646
- }
647
-
648
- /**
649
- * Check whether this request is valid according to its attributes
650
- *
651
- * @return bool|WP_Error
652
- */
653
- public function has_valid_params() {
654
-
655
- $attributes = $this->get_attributes();
656
- $required = array();
657
-
658
- // No arguments set, skip validation
659
- if ( empty( $attributes['args'] ) ) {
660
- return true;
661
- }
662
-
663
- foreach ( $attributes['args'] as $key => $arg ) {
664
-
665
- $param = $this->get_param( $key );
666
- if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) {
667
- $required[] = $key;
668
- }
669
- }
670
-
671
- if ( ! empty( $required ) ) {
672
- return new WP_Error( 'rest_missing_callback_param', sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ), array( 'status' => 400, 'params' => $required ) );
673
- }
674
-
675
- // check the validation callbacks for each registered arg.
676
- // This is done after required checking as required checking is cheaper.
677
- $invalid_params = array();
678
-
679
- foreach ( $attributes['args'] as $key => $arg ) {
680
-
681
- $param = $this->get_param( $key );
682
-
683
- if ( null !== $param && ! empty( $arg['validate_callback']) ) {
684
- $valid_check = call_user_func( $arg['validate_callback'], $param, $this, $key );
685
-
686
- if ( false === $valid_check ) {
687
- $invalid_params[ $key ] = __( 'Invalid param.' );
688
- }
689
-
690
- if ( is_wp_error( $valid_check ) ) {
691
- $invalid_params[] = sprintf( '%s (%s)', $key, $valid_check->get_error_message() );
692
- }
693
- }
694
- }
695
-
696
- if ( $invalid_params ) {
697
- return new WP_Error( 'rest_invalid_param', sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', $invalid_params ) ), array( 'status' => 400, 'params' => $invalid_params ) );
698
- }
699
-
700
- return true;
701
-
702
- }
703
-
704
- /**
705
- * Check if a parameter is set
706
- *
707
- * @param string $key Parameter name
708
- * @return boolean
709
- */
710
- // @codingStandardsIgnoreStart
711
- public function offsetExists( $offset ) {
712
- // @codingStandardsIgnoreEnd
713
- $order = $this->get_parameter_order();
714
-
715
- foreach ( $order as $type ) {
716
- if ( isset( $this->params[ $type ][ $offset ] ) ) {
717
- return true;
718
- }
719
- }
720
-
721
- return false;
722
- }
723
-
724
- /**
725
- * Get a parameter from the request
726
- *
727
- * @param string $key Parameter name
728
- * @return mixed|null Value if set, null otherwise
729
- */
730
- // @codingStandardsIgnoreStart
731
- public function offsetGet( $offset ) {
732
- // @codingStandardsIgnoreEnd
733
- return $this->get_param( $offset );
734
- }
735
-
736
- /**
737
- * Set a parameter on the request
738
- *
739
- * @param string $key Parameter name
740
- * @param mixed $value Parameter value
741
- */
742
- // @codingStandardsIgnoreStart
743
- public function offsetSet( $offset, $value ) {
744
- // @codingStandardsIgnoreEnd
745
- return $this->set_param( $offset, $value );
746
- }
747
-
748
- /**
749
- * Remove a parameter from the request
750
- *
751
- * @param string $key Parameter name
752
- * @param mixed $value Parameter value
753
- */
754
- // @codingStandardsIgnoreStart
755
- public function offsetUnset( $offset ) {
756
- // @codingStandardsIgnoreEnd
757
- $order = $this->get_parameter_order();
758
-
759
- // Remove the offset from every group
760
- foreach ( $order as $type ) {
761
- unset( $this->params[ $type ][ $offset ] );
762
- }
763
- }
764
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lib/infrastructure/class-wp-rest-response.php DELETED
@@ -1,176 +0,0 @@
1
- <?php
2
-
3
- class WP_REST_Response extends WP_HTTP_Response {
4
- /**
5
- * Links related to the response
6
- *
7
- * @var array
8
- */
9
- protected $links = array();
10
-
11
- /**
12
- * The route that was to create the response
13
- *
14
- * @var string
15
- */
16
- protected $matched_route = '';
17
-
18
- /**
19
- * The handler that was used to create the response
20
- *
21
- * @var null|array
22
- */
23
- protected $matched_handler = null;
24
-
25
- /**
26
- * Add a link to the response
27
- *
28
- * @internal The $rel parameter is first, as this looks nicer when sending multiple
29
- *
30
- * @link http://tools.ietf.org/html/rfc5988
31
- * @link http://www.iana.org/assignments/link-relations/link-relations.xml
32
- *
33
- * @param string $rel Link relation. Either an IANA registered type, or an absolute URL
34
- * @param string $link Target IRI for the link
35
- * @param array $attributes Link parameters to send along with the URL
36
- */
37
- public function add_link( $rel, $href, $attributes = array() ) {
38
- if ( empty( $this->links[ $rel ] ) ) {
39
- $this->links[ $rel ] = array();
40
- }
41
-
42
- if ( isset( $attributes['href'] ) ) {
43
- // Remove the href attribute, as it's used for the main URL
44
- unset( $attributes['href'] );
45
- }
46
-
47
- $this->links[ $rel ][] = array(
48
- 'href' => $href,
49
- 'attributes' => $attributes,
50
- );
51
- }
52
-
53
- /**
54
- * Add multiple links to the response.
55
- *
56
- * Link data should be an associative array with link relation as the key.
57
- * The value can either be an associative array of link attributes
58
- * (including `href` with the URL for the response), or a list of these
59
- * associative arrays.
60
- *
61
- * @param array $links Map of link relation to list of links.
62
- */
63
- public function add_links( $links ) {
64
- foreach ( $links as $rel => $set ) {
65
- // If it's a single link, wrap with an array for consistent handling
66
- if ( isset( $set['href'] ) ) {
67
- $set = array( $set );
68
- }
69
-
70
- foreach ( $set as $attributes ) {
71
- $this->add_link( $rel, $attributes['href'], $attributes );
72
- }
73
- }
74
- }
75
-
76
- /**
77
- * Get links for the response
78
- *
79
- * @return array
80
- */
81
- public function get_links() {
82
- return $this->links;
83
- }
84
-
85
- /**
86
- * Set a single link header
87
- *
88
- * @internal The $rel parameter is first, as this looks nicer when sending multiple
89
- *
90
- * @link http://tools.ietf.org/html/rfc5988
91
- * @link http://www.iana.org/assignments/link-relations/link-relations.xml
92
- *
93
- * @param string $rel Link relation. Either an IANA registered type, or an absolute URL
94
- * @param string $link Target IRI for the link
95
- * @param array $other Other parameters to send, as an assocative array
96
- */
97
- public function link_header( $rel, $link, $other = array() ) {
98
- $header = '<' . $link . '>; rel="' . $rel . '"';
99
-
100
- foreach ( $other as $key => $value ) {
101
- if ( 'title' === $key ) {
102
- $value = '"' . $value . '"';
103
- }
104
- $header .= '; ' . $key . '=' . $value;
105
- }
106
- return $this->header( 'Link', $header, false );
107
- }
108
-
109
- /**
110
- * Get the route that was used to
111
- *
112
- * @return string
113
- */
114
- public function get_matched_route() {
115
- return $this->matched_route;
116
- }
117
-
118
- /**
119
- * Set the route (regex for path) that caused the response
120
- *
121
- * @param string $route
122
- */
123
- public function set_matched_route( $route ) {
124
- $this->matched_route = $route;
125
- }
126
-
127
- /**
128
- * Get the handler that was used to generate the response
129
- *
130
- * @return null|array
131
- */
132
- public function get_matched_handler() {
133
- return $this->matched_handler;
134
- }
135
-
136
- /**
137
- * Get the handler that was responsible for generting the response
138
- *
139
- * @param array $handler
140
- */
141
- public function set_matched_handler( $handler ) {
142
- $this->matched_handler = $handler;
143
- }
144
-
145
- /**
146
- * Check if the response is an error, i.e. >= 400 response code
147
- *
148
- * @return boolean
149
- */
150
- public function is_error() {
151
- return $this->get_status() >= 400;
152
- }
153
-
154
- /**
155
- * Get a WP_Error object from the response's
156
- *
157
- * @return WP_Error|null on not an errored response
158
- */
159
- public function as_error() {
160
- if ( ! $this->is_error() ) {
161
- return null;
162
- }
163
-
164
- $error = new WP_Error;
165
-
166
- if ( is_array( $this->get_data() ) ) {
167
- foreach ( $this->get_data() as $err ) {
168
- $error->add( $err['code'], $err['message'], $err['data'] );
169
- }
170
- } else {
171
- $error->add( $this->get_status(), '', array( 'status' => $this->get_status() ) );
172
- }
173
-
174
- return $error;
175
- }
176
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lib/infrastructure/class-wp-rest-server.php DELETED
@@ -1,962 +0,0 @@
1
- <?php
2
- /**
3
- * WordPress REST API
4
- *
5
- * Contains the WP_REST_Server class.
6
- *
7
- * @package WordPress
8
- */
9
-
10
- require_once ( ABSPATH . 'wp-admin/includes/admin.php' );
11
-
12
- /**
13
- * WordPress REST API server handler
14
- *
15
- * @package WordPress
16
- */
17
- class WP_REST_Server {
18
- const METHOD_GET = 'GET';
19
- const METHOD_POST = 'POST';
20
- const METHOD_PUT = 'PUT';
21
- const METHOD_PATCH = 'PATCH';
22
- const METHOD_DELETE = 'DELETE';
23
-
24
- const READABLE = 'GET';
25
- const CREATABLE = 'POST';
26
- const EDITABLE = 'POST, PUT, PATCH';
27
- const DELETABLE = 'DELETE';
28
- const ALLMETHODS = 'GET, POST, PUT, PATCH, DELETE';
29
-
30
- /**
31
- * Does the endpoint accept raw JSON entities?
32
- */
33
- const ACCEPT_RAW = 64;
34
- const ACCEPT_JSON = 128;
35
-
36
- /**
37
- * Should we hide this endpoint from the index?
38
- */
39
- const HIDDEN_ENDPOINT = 256;
40
-
41
- /**
42
- * Map of HTTP verbs to constants
43
- * @var array
44
- */
45
- public static $method_map = array(
46
- 'HEAD' => self::METHOD_GET,
47
- 'GET' => self::METHOD_GET,
48
- 'POST' => self::METHOD_POST,
49
- 'PUT' => self::METHOD_PUT,
50
- 'PATCH' => self::METHOD_PATCH,
51
- 'DELETE' => self::METHOD_DELETE,
52
- );
53
-
54
- /**
55
- * Namespaces registered to the server
56
- *
57
- * @var array
58
- */
59
- protected $namespaces = array();
60
-
61
- /**
62
- * Endpoints registered to the server
63
- *
64
- * @var array
65
- */
66
- protected $endpoints = array();
67
-
68
- /**
69
- * Options defined for the routes
70
- *
71
- * @var array
72
- */
73
- protected $route_options = array();
74
-
75
- /**
76
- * Instantiate the server
77
- */
78
- public function __construct() {
79
- $this->endpoints = array(
80
- // Meta endpoints
81
- '/' => array(
82
- 'callback' => array( $this, 'get_index' ),
83
- 'methods' => 'GET',
84
- ),
85
- );
86
- }
87
-
88
-
89
- /**
90
- * Check the authentication headers if supplied
91
- *
92
- * @return WP_Error|null WP_Error indicates unsuccessful login, null indicates successful or no authentication provided
93
- */
94
- public function check_authentication() {
95
- /**
96
- * Pass an authentication error to the API
97
- *
98
- * This is used to pass a {@see WP_Error} from an authentication method
99
- * back to the API.
100
- *
101
- * Authentication methods should check first if they're being used, as
102
- * multiple authentication methods can be enabled on a site (cookies,
103
- * HTTP basic auth, OAuth). If the authentication method hooked in is
104
- * not actually being attempted, null should be returned to indicate
105
- * another authentication method should check instead. Similarly,
106
- * callbacks should ensure the value is `null` before checking for
107
- * errors.
108
- *
109
- * A {@see WP_Error} instance can be returned if an error occurs, and
110
- * this should match the format used by API methods internally (that is,
111
- * the `status` data should be used). A callback can return `true` to
112
- * indicate that the authentication method was used, and it succeeded.
113
- *
114
- * @param WP_Error|null|boolean WP_Error if authentication error, null if authentication method wasn't used, true if authentication succeeded
115
- */
116
- return apply_filters( 'rest_authentication_errors', null );
117
- }
118
-
119
- /**
120
- * Convert an error to a response object
121
- *
122
- * This iterates over all error codes and messages to change it into a flat
123
- * array. This enables simpler client behaviour, as it is represented as a
124
- * list in JSON rather than an object/map
125
- *
126
- * @param WP_Error $error
127
- * @return array List of associative arrays with code and message keys
128
- */
129
- protected function error_to_response( $error ) {
130
- $error_data = $error->get_error_data();
131
- if ( is_array( $error_data ) && isset( $error_data['status'] ) ) {
132
- $status = $error_data['status'];
133
- } else {
134
- $status = 500;
135
- }
136
-
137
- $data = array();
138
- foreach ( (array) $error->errors as $code => $messages ) {
139
- foreach ( (array) $messages as $message ) {
140
- $data[] = array( 'code' => $code, 'message' => $message, 'data' => $error->get_error_data( $code ) );
141
- }
142
- }
143
- $response = new WP_REST_Response( $data, $status );
144
-
145
- return $response;
146
- }
147
-
148
- /**
149
- * Get an appropriate error representation in JSON
150
- *
151
- * Note: This should only be used in {@see WP_REST_Server::serve_request()},
152
- * as it cannot handle WP_Error internally. All callbacks and other internal
153
- * methods should instead return a WP_Error with the data set to an array
154
- * that includes a 'status' key, with the value being the HTTP status to
155
- * send.
156
- *
157
- * @param string $code WP_Error-style code
158
- * @param string $message Human-readable message
159
- * @param int $status HTTP status code to send
160
- * @return string JSON representation of the error
161
- */
162
- protected function json_error( $code, $message, $status = null ) {
163
- if ( $status ) {
164
- $this->set_status( $status );
165
- }
166
- $error = compact( 'code', 'message' );
167
-
168
- return json_encode( array( $error ) );
169
- }
170
-
171
- /**
172
- * Handle serving an API request
173
- *
174
- * Matches the current server URI to a route and runs the first matching
175
- * callback then outputs a JSON representation of the returned value.
176
- *
177
- * @uses WP_REST_Server::dispatch()
178
- */
179
- public function serve_request( $path = null ) {
180
- $content_type = isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json';
181
- $this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
182
-
183
- // Mitigate possible JSONP Flash attacks
184
- // http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
185
- $this->send_header( 'X-Content-Type-Options', 'nosniff' );
186
-
187
- // Proper filter for turning off the JSON API. It is on by default.
188
- $enabled = apply_filters( 'rest_enabled', true );
189
-
190
- $jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
191
-
192
- if ( ! $enabled ) {
193
- echo $this->json_error( 'rest_disabled', __( 'The REST API is disabled on this site.' ), 404 );
194
- return false;
195
- }
196
- if ( isset( $_GET['_jsonp'] ) ) {
197
- if ( ! $jsonp_enabled ) {
198
- echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
199
- return false;
200
- }
201
-
202
- // Check for invalid characters (only alphanumeric allowed)
203
- if ( ! is_string( $_GET['_jsonp'] ) || preg_match( '/\W\./', $_GET['_jsonp'] ) ) {
204
- echo $this->json_error( 'rest_callback_invalid', __( 'The JSONP callback function is invalid.' ), 400 );
205
- return false;
206
- }
207
- }
208
-
209
- if ( empty( $path ) ) {
210
- if ( isset( $_SERVER['PATH_INFO'] ) ) {
211
- $path = $_SERVER['PATH_INFO'];
212
- } else {
213
- $path = '/';
214
- }
215
- }
216
-
217
- $request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );
218
- $request->set_query_params( $_GET );
219
- $request->set_body_params( $_POST );
220
- $request->set_file_params( $_FILES );
221
- $request->set_headers( $this->get_headers( $_SERVER ) );
222
- $request->set_body( $this->get_raw_data() );
223
-
224
- /**
225
- * HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
226
- * $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
227
- * header.
228
- */
229
- if ( isset( $_GET['_method'] ) ) {
230
- $request->set_method( $_GET['_method'] );
231
- } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
232
- $request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
233
- }
234
-
235
- $result = $this->check_authentication();
236
-
237
- if ( ! is_wp_error( $result ) ) {
238
- /**
239
- * Allow hijacking the request before dispatching
240
- *
241
- * If `$result` is non-empty, this value will be used to serve the
242
- * request instead.
243
- *
244
- * @param mixed $result Response to replace the requested version with. Can be anything a normal endpoint can return, or null to not hijack the request.
245
- * @param WP_REST_Server $this Server instance
246
- * @param WP_REST_Request $request Request used to generate the response
247
- */
248
- $result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
249
- }
250
-
251
- if ( empty( $result ) ) {
252
- $result = $this->dispatch( $request );
253
- }
254
-
255
- // Normalize to either WP_Error or WP_REST_Response...
256
- $result = rest_ensure_response( $result );
257
-
258
- // ...then convert WP_Error across
259
- if ( is_wp_error( $result ) ) {
260
- $result = $this->error_to_response( $result );
261
- }
262
-
263
- /**
264
- * Allow modifying the response before returning
265
- *
266
- * @param WP_HTTP_ResponseInterface $result Result to send to the client. Usually a WP_REST_Response
267
- * @param WP_REST_Server $this Server instance
268
- * @param WP_REST_Request $request Request used to generate the response
269
- */
270
- $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );
271
-
272
- // Wrap the response in an envelope if asked for
273
- if ( isset( $_GET['_envelope'] ) ) {
274
- $result = $this->envelope_response( $result, isset( $_GET['_embed'] ) );
275
- }
276
-
277
- // Send extra data from response objects
278
- $headers = $result->get_headers();
279
- $this->send_headers( $headers );
280
-
281
- $code = $result->get_status();
282
- $this->set_status( $code );
283
-
284
- /**
285
- * Allow sending the request manually
286
- *
287
- * If `$served` is true, the result will not be sent to the client.
288
- *
289
- * This is a filter rather than an action, since this is designed to be
290
- * re-entrant if needed.
291
- *
292
- * @param bool $served Whether the request has already been served
293
- * @param WP_HTTP_ResponseInterface $result Result to send to the client. Usually a WP_REST_Response
294
- * @param WP_REST_Request $request Request used to generate the response
295
- * @param WP_REST_Server $this Server instance
296
- */
297
- $served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );
298
-
299
- if ( ! $served ) {
300
- if ( 'HEAD' === $request->get_method() ) {
301
- return;
302
- }
303
-
304
- // Embed links inside the request
305
- $result = $this->response_to_data( $result, isset( $_GET['_embed'] ) );
306
-
307
- $result = json_encode( $result );
308
-
309
- $json_error_message = $this->get_json_last_error();
310
- if ( $json_error_message ) {
311
- $json_error_obj = new WP_Error( 'rest_encode_error', $json_error_message, array( 'status' => 500 ) );
312
- $result = $this->error_to_response( $json_error_obj );
313
- $result = json_encode( $result->data[0] );
314
- }
315
-
316
- if ( isset( $_GET['_jsonp'] ) ) {
317
- // Prepend '/**/' to mitigate possible JSONP Flash attacks
318
- // http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
319
- echo '/**/' . $_GET['_jsonp'] . '(' . $result . ')';
320
- } else {
321
- echo $result;
322
- }
323
- }
324
- }
325
-
326
- /**
327
- * Convert a response to data to send
328
- *
329
- * @param WP_REST_Response $response Response object
330
- * @param boolean $embed Should we embed links?
331
- * @return array
332
- */
333
- public function response_to_data( $response, $embed ) {
334
- $data = $this->prepare_response( $response->get_data() );
335
- $links = $this->get_response_links( $response );
336
-
337
- if ( ! empty( $links ) ) {
338
- // Convert links to part of the data
339
- $data['_links'] = $links;
340
-
341
- if ( $embed ) {
342
- $data = $this->embed_links( $data );
343
- }
344
- }
345
-
346
- return $data;
347
- }
348
-
349
- /**
350
- * Get links from a response.
351
- *
352
- * Extracts the links from a reponse into a structured hash, suitable for
353
- * direct output.
354
- *
355
- * @param WP_REST_Response $response Response to extract links from.
356
- * @return array Map of link relation to list of link hashes.
357
- */
358
- public static function get_response_links( $response ) {
359
- $links = $response->get_links();
360
-
361
- if ( empty( $links ) ) {
362
- return array();
363
- }
364
-
365
- // Convert links to part of the data
366
- $data = array();
367
- foreach ( $links as $rel => $items ) {
368
- $data[ $rel ] = array();
369
-
370
- foreach ( $items as $item ) {
371
- $attributes = $item['attributes'];
372
- $attributes['href'] = $item['href'];
373
- $data[ $rel ][] = $attributes;
374
- }
375
- }
376
-
377
- return $data;
378
- }
379
-
380
- /**
381
- * Embed the links from the data into the request
382
- *
383
- * @param array $data Data from the request
384
- * @return array Data with sub-requests embedded
385
- */
386
- protected function embed_links( $data ) {
387
- if ( empty( $data['_links'] ) ) {
388
- return $data;
389
- }
390
-
391
- $embedded = array();
392
- $api_root = rest_url();
393
- foreach ( $data['_links'] as $rel => $links ) {
394
- // Ignore links to self, for obvious reasons
395
- if ( 'self' === $rel ) {
396
- continue;
397
- }
398
-
399
- $embeds = array();
400
-
401
- foreach ( $links as $item ) {
402
- // Is the link embeddable?
403
- if ( empty( $item['embeddable'] ) || strpos( $item['href'], $api_root ) !== 0 ) {
404
- // Ensure we keep the same order
405
- $embeds[] = array();
406
- continue;
407
- }
408
-
409
- // Run through our internal routing and serve
410
- $route = substr( $item['href'], strlen( untrailingslashit( $api_root ) ) );
411
- $query_params = array();
412
-
413
- // Parse out URL query parameters
414
- $parsed = parse_url( $route );
415
- if ( empty( $parsed['path'] ) ) {
416
- $embeds[] = array();
417
- continue;
418
- }
419
-
420
- if ( ! empty( $parsed['query'] ) ) {
421
- parse_str( $parsed['query'], $query_params );
422
-
423
- // Ensure magic quotes are stripped
424
- // @codeCoverageIgnoreStart
425
- if ( get_magic_quotes_gpc() ) {
426
- $query_params = stripslashes_deep( $query_params );
427
- }
428
- // @codeCoverageIgnoreEnd
429
- }
430
-
431
- // Embedded resources get passed context=embed
432
- $query_params['context'] = 'embed';
433
-
434
- $request = new WP_REST_Request( 'GET', $parsed['path'] );
435
- $request->set_query_params( $query_params );
436
- $response = $this->dispatch( $request );
437
-
438
- $embeds[] = $response;
439
- }
440
-
441
- // Did we get any real links?
442
- $has_links = count( array_filter( $embeds ) );
443
- if ( $has_links ) {
444
- $embedded[ $rel ] = $embeds;
445
- }
446
- }
447
-
448
- if ( ! empty( $embedded ) ) {
449
- $data['_embedded'] = $embedded;
450
- }
451
-
452
- return $data;
453
- }
454
-
455
- /**
456
- * Wrap the response in an envelope
457
- *
458
- * The enveloping technique is used to work around browser/client
459
- * compatibility issues. Essentially, it converts the full HTTP response to
460
- * data instead.
461
- *
462
- * @param WP_REST_Response $response Response object
463
- * @param boolean $embed Should we embed links?
464
- * @return WP_REST_Response New reponse with wrapped data
465
- */
466
- public function envelope_response( $response, $embed ) {
467
- $envelope = array(
468
- 'body' => $this->response_to_data( $response, $embed ),
469
- 'status' => $response->get_status(),
470
- 'headers' => $response->get_headers(),
471
- );
472
-
473
- /**
474
- * Alter the enveloped form of a response
475
- *
476
- * @param array $envelope Envelope data
477
- * @param WP_REST_Response $response Original response data
478
- */
479
- $envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
480
-
481
- // Ensure it's still a response
482
- return rest_ensure_response( $envelope );
483
- }
484
-
485
- /**
486
- * Register a route to the server
487
- *
488
- * @param string $route
489
- * @param array $route_args
490
- * @param boolean $override If the route already exists, should we override it? True overrides, false merges (with newer overriding if duplicate keys exist)
491
- */
492
- public function register_route( $namespace, $route, $route_args, $override = false ) {
493
- if ( ! isset( $this->namespaces[ $namespace ] ) ) {
494
- $this->namespaces[ $namespace ] = array();
495
-
496
- $this->register_route( $namespace, '/' . $namespace, array(
497
- array(
498
- 'methods' => self::READABLE,
499
- 'callback' => array( $this, 'get_namespace_index' ),
500
- 'args' => array(
501
- 'namespace' => array(
502
- 'default' => $namespace,
503
- ),
504
- ),
505
- ),
506
- ) );
507
- }
508
-
509
- // Associative to avoid double-registration
510
- $this->namespaces[ $namespace ][ $route ] = true;
511
- $route_args['namespace'] = $namespace;
512
-
513
- if ( $override || empty( $this->endpoints[ $route ] ) ) {
514
- $this->endpoints[ $route ] = $route_args;
515
- } else {
516
- $this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
517
- }
518
- }
519
-
520
- /**
521
- * Retrieve the route map
522
- *
523
- * The route map is an associative array with path regexes as the keys. The
524
- * value is an indexed array with the callback function/method as the first
525
- * item, and a bitmask of HTTP methods as the second item (see the class
526
- * constants).
527
- *
528
- * Each route can be mapped to more than one callback by using an array of
529
- * the indexed arrays. This allows mapping e.g. GET requests to one callback
530
- * and POST requests to another.
531
- *
532
- * Note that the path regexes (array keys) must have @ escaped, as this is
533
- * used as the delimiter with preg_match()
534
- *
535
- * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
536
- */
537
- public function get_routes() {
538
-
539
- $endpoints = apply_filters( 'rest_endpoints', $this->endpoints );
540
-
541
- // Normalise the endpoints
542
- $defaults = array(
543
- 'methods' => '',
544
- 'accept_json' => false,
545
- 'accept_raw' => false,
546
- 'show_in_index' => true,
547
- 'args' => array(),
548
- );
549
- foreach ( $endpoints as $route => &$handlers ) {
550
- if ( isset( $handlers['callback'] ) ) {
551
- // Single endpoint, add one deeper
552
- $handlers = array( $handlers );
553
- }
554
- if ( ! isset( $this->route_options[ $route ] ) ) {
555
- $this->route_options[ $route ] = array();
556
- }
557
-
558
- foreach ( $handlers as $key => &$handler ) {
559
- if ( ! is_numeric( $key ) ) {
560
- // Route option, move it to the options
561
- $this->route_options[ $route ][ $key ] = $handler;
562
- unset( $handlers[ $key ] );
563
- continue;
564
- }
565
- $handler = wp_parse_args( $handler, $defaults );
566
-
567
- // Allow comma-separated HTTP methods
568
- if ( is_string( $handler['methods'] ) ) {
569
- $methods = explode( ',', $handler['methods'] );
570
- } else if ( is_array( $handler['methods'] ) ) {
571
- $methods = $handler['methods'];
572
- }
573
-
574
- $handler['methods'] = array();
575
- foreach ( $methods as $method ) {
576
- $method = strtoupper( trim( $method ) );
577
- $handler['methods'][ $method ] = true;
578
- }
579
- }
580
- }
581
- return $endpoints;
582
- }
583
-
584
- /**
585
- * Get namespaces registered on the server.
586
- *
587
- * @return array List of registered namespaces.
588
- */
589
- public function get_namespaces() {
590
- return array_keys( $this->namespaces );
591
- }
592
-
593
- /**
594
- * Match the request to a callback and call it
595
- *
596
- * @param WP_REST_Request $request Request to attempt dispatching
597
- * @return WP_REST_Response Response returned by the callback
598
- */
599
- public function dispatch( $request ) {
600
- $method = $request->get_method();
601
- $path = $request->get_route();
602
-
603
- foreach ( $this->get_routes() as $route => $handlers ) {
604
- foreach ( $handlers as $handler ) {
605
- $callback = $handler['callback'];
606
- $supported = $handler['methods'];
607
- $response = null;
608
-
609
- if ( empty( $handler['methods'][ $method ] ) ) {
610
- continue;
611
- }
612
-
613
- $match = preg_match( '@^' . $route . '$@i', $path, $args );
614
-
615
- if ( ! $match ) {
616
- continue;
617
- }
618
-
619
- if ( ! is_callable( $callback ) ) {
620
- $response = new WP_Error( 'rest_invalid_handler', __( 'The handler for the route is invalid' ), array( 'status' => 500 ) );
621
- }
622
-
623
- if ( ! is_wp_error( $response ) ) {
624
-
625
- $request->set_url_params( $args );
626
- $request->set_attributes( $handler );
627
-
628
- $request->sanitize_params();
629
-
630
- $defaults = array();
631
-
632
- foreach ( $handler['args'] as $arg => $options ) {
633
- if ( isset( $options['default'] ) ) {
634
- $defaults[ $arg ] = $options['default'];
635
- }
636
- }
637
-
638
- $request->set_default_params( $defaults );
639
-
640
- $check_required = $request->has_valid_params();
641
- if ( is_wp_error( $check_required ) ) {
642
- $response = $check_required;
643
- }
644
- }
645
-
646
- if ( ! is_wp_error( $response ) ) {
647
- // check permission specified on the route.
648
- if ( ! empty( $handler['permission_callback'] ) ) {
649
- $permission = call_user_func( $handler['permission_callback'], $request );
650
-
651
- if ( is_wp_error( $permission ) ) {
652
- $response = $permission;
653
- } else if ( false === $permission || null === $permission ) {
654
- $response = new WP_Error( 'rest_forbidden', __( "You don't have permission to do this." ), array( 'status' => 403 ) );
655
- }
656
- }
657
- }
658
-
659
- if ( ! is_wp_error( $response ) ) {
660
- /**
661
- * Allow plugins to override dispatching the request
662
- *
663
- * @param boolean $dispatch_result Dispatch result, will be used if not empty
664
- * @param WP_REST_Request $request
665
- */
666
- $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request );
667
-
668
- // Allow plugins to halt the request via this filter
669
- if ( null !== $dispatch_result ) {
670
- $response = $dispatch_result;
671
- } else {
672
- $response = call_user_func( $callback, $request );
673
- }
674
- }
675
-
676
- if ( is_wp_error( $response ) ) {
677
- $response = $this->error_to_response( $response );
678
- } else {
679
- $response = rest_ensure_response( $response );
680
- }
681
-
682
- $response->set_matched_route( $route );
683
- $response->set_matched_handler( $handler );
684
-
685
- return $response;
686
- }
687
- }
688
-
689
- return $this->error_to_response( new WP_Error( 'rest_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) ) );
690
- }
691
-
692
- /**
693
- * Returns if an error occurred during most recent JSON encode/decode
694
- * Strings to be translated will be in format like "Encoding error: Maximum stack depth exceeded"
695
- *
696
- * @return boolean|string Boolean false or string error message
697
- */
698
- protected function get_json_last_error( ) {
699
- // see https://core.trac.wordpress.org/ticket/27799
700
- if ( ! function_exists( 'json_last_error' ) ) {
701
- return false;
702
- }
703
-
704
- $last_error_code = json_last_error();
705
- if ( ( defined( 'JSON_ERROR_NONE' ) && JSON_ERROR_NONE === $last_error_code ) || empty( $last_error_code ) ) {
706
- return false;
707
- }
708
-
709
- return json_last_error_msg();
710
- }
711
-
712
- /**
713
- * Get the site index.
714
- *
715
- * This endpoint describes the capabilities of the site.
716
- *
717
- * @todo Should we generate text documentation too based on PHPDoc?
718
- *
719
- * @return array Index entity
720
- */
721
- public function get_index() {
722
- // General site data
723
- $available = array(
724
- 'name' => get_option( 'blogname' ),
725
- 'description' => get_option( 'blogdescription' ),
726
- 'url' => get_option( 'siteurl' ),
727
- 'namespaces' => array_keys( $this->namespaces ),
728
- 'authentication' => array(),
729
- 'routes' => $this->get_route_data( $this->get_routes() ),
730
- );
731
-
732
- $response = new WP_REST_Response( $available );
733
- $response->add_link( 'help', 'http://v2.wp-api.org/' );
734
-
735
- /**
736
- * Filter the API root index data.
737
- *
738
- * This contains the data describing the API. This includes information
739
- * about supported authentication schemes, supported namespaces, routes
740
- * available on the API, and a small amount of data about the site.
741
- *
742
- * @param WP_REST_Response $response Response data.
743
- */
744
- return apply_filters( 'rest_index', $response );
745
- }
746
-
747
- /**
748
- * Get the index for a namespace.
749
- *
750
- * @param WP_REST_Request $request
751
- * @return array|WP_REST_Response
752
- */
753
- public function get_namespace_index( $request ) {
754
- $namespace = $request['namespace'];
755
-
756
- if ( ! isset( $this->namespaces[ $namespace ] ) ) {
757
- return new WP_Error( 'rest_invalid_namespace', __( 'The specified namespace could not be found.' ), array( 'status' => 404 ) );
758
- }
759
-
760
- $routes = $this->namespaces[ $namespace ];
761
- $endpoints = array_intersect_key( $this->get_routes(), $routes );
762
-
763
- $data = array(
764
- 'namespace' => $namespace,
765
- 'routes' => $this->get_route_data( $endpoints ),
766
- );
767
- $response = rest_ensure_response( $data );
768
-
769
- // Link to the root index
770
- $response->add_link( 'up', rest_url( '/' ) );
771
-
772
- /**
773
- * Filter the namespace index data.
774
- *
775
- * This typically is just the route data for the namespace, but you can
776
- * add any data you'd like here.
777
- *
778
- * @param WP_REST_Response $response Response data.
779
- * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
780
- */
781
- return apply_filters( 'rest_namespace_index', $response, $request );
782
- }
783
-
784
- /**
785
- * Get the publicly-visible data for routes.
786
- *
787
- * @param array $routes Routes to get data for
788
- * @return array Route data to expose in indexes.
789
- */
790
- protected function get_route_data( $routes ) {
791
- $available = array();
792
- // Find the available routes
793
- foreach ( $routes as $route => $callbacks ) {
794
- $data = array(
795
- 'namespace' => '',
796
- 'methods' => array(),
797
- );
798
- if ( isset( $this->route_options[ $route ] ) ) {
799
- $options = $this->route_options[ $route ];
800
- if ( isset( $options['namespace'] ) ) {
801
- $data['namespace'] = $options['namespace'];
802
- }
803
- }
804
-
805
- $route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
806
-
807
- foreach ( $callbacks as $callback ) {
808
- // Skip to the next route if any callback is hidden
809
- if ( empty( $callback['show_in_index'] ) ) {
810
- continue;
811
- }
812
-
813
- $data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
814
-
815
- // For non-variable routes, generate links
816
- if ( strpos( $route, '{' ) === false ) {
817
- $data['_links'] = array(
818
- 'self' => rest_url( $route ),
819
- );
820
- }
821
- }
822
-
823
- if ( empty( $data['methods'] ) ) {
824
- // No methods supported, hide the route
825
- continue;
826
- }
827
-
828
- $available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
829
- }
830
-
831
- /**
832
- * Filter the publicly-visible data for routes.
833
- *
834
- * This data is exposed on indexes and can be used by clients or
835
- * developers to investigate the site and find out how to use it. It
836
- * acts as a form of self-documentation.
837
- *
838
- * @param array $available Map of route to route data.
839
- * @param array $routes Internal route data as an associative array.
840
- */
841
- return apply_filters( 'rest_route_data', $available, $routes );
842
- }
843
-
844
- /**
845
- * Send a HTTP status code
846
- *
847
- * @param int $code HTTP status
848
- */
849
- protected function set_status( $code ) {
850
- status_header( $code );
851
- }
852
-
853
- /**
854
- * Send a HTTP header
855
- *
856
- * @param string $key Header key
857
- * @param string $value Header value
858
- */
859
- public function send_header( $key, $value ) {
860
- // Sanitize as per RFC2616 (Section 4.2):
861
- // Any LWS that occurs between field-content MAY be replaced with a
862
- // single SP before interpreting the field value or forwarding the
863
- // message downstream.
864
- $value = preg_replace( '/\s+/', ' ', $value );
865
- header( sprintf( '%s: %s', $key, $value ) );
866
- }
867
-
868
- /**
869
- * Send multiple HTTP headers
870
- *
871
- * @param array Map of header name to header value
872
- */
873
- public function send_headers( $headers ) {
874
- foreach ( $headers as $key => $value ) {
875
- $this->send_header( $key, $value );
876
- }
877
- }
878
-
879
- /**
880
- * Retrieve the raw request entity (body)
881
- *
882
- * @return string
883
- */
884
- public function get_raw_data() {
885
- global $HTTP_RAW_POST_DATA;
886
-
887
- // A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
888
- // but we can do it ourself.
889
- if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
890
- $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
891
- }
892
-
893
- return $HTTP_RAW_POST_DATA;
894
- }
895
-
896
- /**
897
- * Prepares response data to be serialized to JSON
898
- *
899
- * This supports the JsonSerializable interface for PHP 5.2-5.3 as well.
900
- *
901
- * @codeCoverageIgnore This is a compatibility shim.
902
- *
903
- * @param mixed $data Native representation
904
- * @return array|string Data ready for `json_encode()`
905
- */
906
- public function prepare_response( $data ) {
907
- if ( ! defined( 'WP_REST_SERIALIZE_COMPATIBLE' ) || WP_REST_SERIALIZE_COMPATIBLE === false ) {
908
- return $data;
909
- }
910
-
911
- switch ( gettype( $data ) ) {
912
- case 'boolean':
913
- case 'integer':
914
- case 'double':
915
- case 'string':
916
- case 'NULL':
917
- // These values can be passed through
918
- return $data;
919
-
920
- case 'array':
921
- // Arrays must be mapped in case they also return objects
922
- return array_map( array( $this, 'prepare_response' ), $data );
923
-
924
- case 'object':
925
- if ( $data instanceof JsonSerializable ) {
926
- $data = $data->jsonSerialize();
927
- } else {
928
- $data = get_object_vars( $data );
929
- }
930
-
931
- // Now, pass the array (or whatever was returned from
932
- // jsonSerialize through.)
933
- return $this->prepare_response( $data );
934
-
935
- default:
936
- return null;
937
- }
938
- }
939
-
940
- /**
941
- * Extract headers from a PHP-style $_SERVER array
942
- *
943
- * @param array $server Associative array similar to $_SERVER
944
- * @return array Headers extracted from the input
945
- */
946
- public function get_headers( $server ) {
947
- $headers = array();
948
-
949
- // CONTENT_* headers are not prefixed with HTTP_
950
- $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
951
-
952
- foreach ( $server as $key => $value ) {
953
- if ( strpos( $key, 'HTTP_' ) === 0 ) {
954
- $headers[ substr( $key, 5 ) ] = $value;
955
- } elseif ( isset( $additional[ $key ] ) ) {
956
- $headers[ $key ] = $value;
957
- }
958
- }
959
-
960
- return $headers;
961
- }
962
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
plugin.php CHANGED
@@ -4,147 +4,145 @@
4
  * Description: JSON-based REST API for WordPress, developed as part of GSoC 2013.
5
  * Author: WP REST API Team
6
  * Author URI: http://wp-api.org
7
- * Version: 2.0-beta3.1
8
  * Plugin URI: https://github.com/WP-API/WP-API
9
  * License: GPL2+
10
  */
11
 
12
  /**
13
- * Version number for our API.
14
- *
15
- * @var string
16
  */
17
- define( 'REST_API_VERSION', '2.0-beta3.1' );
18
 
19
  /**
20
- * Include our files for the API.
21
  */
22
- include_once( dirname( __FILE__ ) . '/compatibility-v1.php' );
23
- include_once( dirname( __FILE__ ) . '/lib/infrastructure/class-jsonserializable.php' );
24
-
25
- include_once( dirname( __FILE__ ) . '/lib/infrastructure/class-wp-rest-server.php' );
26
-
27
- include_once( dirname( __FILE__ ) . '/lib/infrastructure/class-wp-http-responseinterface.php' );
28
- include_once( dirname( __FILE__ ) . '/lib/infrastructure/class-wp-http-response.php' );
29
- include_once( dirname( __FILE__ ) . '/lib/infrastructure/class-wp-rest-response.php' );
30
- require_once( dirname( __FILE__ ) . '/lib/infrastructure/class-wp-rest-request.php' );
31
-
32
- require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-controller.php';
33
  require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-posts-controller.php';
34
- require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-attachments-controller.php';
35
- require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-post-types-controller.php';
36
- require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-post-statuses-controller.php';
37
- require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-revisions-controller.php';
38
- require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-taxonomies-controller.php';
39
- require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-terms-controller.php';
40
- require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-users-controller.php';
41
- require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-comments-controller.php';
42
- include_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-meta-controller.php';
43
- include_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-meta-posts-controller.php';
44
- include_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-posts-terms-controller.php';
45
 
46
- include_once( dirname( __FILE__ ) . '/extras.php' );
 
 
 
47
 
 
 
 
 
48
 
49
  /**
50
- * Register a REST API route
51
- *
52
- * @param string $namespace The first URL segment after core prefix. Should be unique to your package/plugin.
53
- * @param string $route The base URL for route you are adding.
54
- * @param array $args Either an array of options for the endpoint, or an array of arrays for multiple methods
55
- * @param boolean $override If the route already exists, should we override it? True overrides, false merges (with newer overriding if duplicate keys exist)
56
  */
57
- function register_rest_route( $namespace, $route, $args = array(), $override = false ) {
58
 
59
- /** @var WP_REST_Server $wp_rest_server */
60
- global $wp_rest_server;
 
 
61
 
62
- if ( isset( $args['callback'] ) ) {
63
- // Upgrade a single set to multiple
64
- $args = array( $args );
65
- }
66
 
67
- $defaults = array(
68
- 'methods' => 'GET',
69
- 'callback' => null,
70
- 'args' => array(),
71
- );
72
- foreach ( $args as &$arg_group ) {
73
- $arg_group = array_merge( $defaults, $arg_group );
74
- }
75
 
76
- $full_route = '/' . trim( $namespace, '/' ) . '/' . trim( $route, '/' );
77
- $wp_rest_server->register_route( $namespace, $full_route, $args, $override );
78
- }
 
79
 
80
  /**
81
- * Register a new field on an existing WordPress object type
82
- *
83
- * @param string|array $object_type "post"|"term"|"comment" etc
84
- * @param string $attribute The attribute name
85
- * @param array $args
86
- * @return bool|wp_error
87
  */
88
- function register_api_field( $object_type, $attribute, $args = array() ) {
89
 
90
- $defaults = array(
91
- 'get_callback' => null,
92
- 'update_callback' => null,
93
- 'schema' => null,
94
- );
95
 
96
- $args = wp_parse_args( $args, $defaults );
 
 
 
97
 
98
- global $wp_rest_additional_fields;
 
 
 
99
 
100
- $object_types = (array) $object_type;
 
 
 
101
 
102
- foreach ( $object_types as $object_type ) {
103
- $wp_rest_additional_fields[ $object_type ][ $attribute ] = $args;
104
- }
105
- }
106
 
107
  /**
108
- * Add the extra Post Type registration arguments we need
 
109
  * These attributes will eventually be committed to core.
 
 
 
 
110
  */
111
  function _add_extra_api_post_type_arguments() {
112
  global $wp_post_types;
113
 
114
- $wp_post_types['post']->show_in_rest = true;
115
- $wp_post_types['post']->rest_base = 'posts';
116
- $wp_post_types['post']->rest_controller_class = 'WP_REST_Posts_Controller';
117
-
118
- $wp_post_types['page']->show_in_rest = true;
119
- $wp_post_types['page']->rest_base = 'pages';
120
- $wp_post_types['page']->rest_controller_class = 'WP_REST_Posts_Controller';
121
 
122
- $wp_post_types['attachment']->show_in_rest = true;
123
- $wp_post_types['attachment']->rest_base = 'media';
124
- $wp_post_types['attachment']->rest_controller_class = 'WP_REST_Attachments_Controller';
 
 
125
 
 
 
 
 
 
126
  }
127
- add_action( 'init', '_add_extra_api_post_type_arguments', 11 );
128
 
129
  /**
130
- * Add the extra Taxonomy registration arguments we need.
 
131
  * These attributes will eventually be committed to core.
 
 
 
 
132
  */
133
  function _add_extra_api_taxonomy_arguments() {
134
  global $wp_taxonomies;
135
 
136
- $wp_taxonomies['category']->show_in_rest = true;
137
- $wp_taxonomies['category']->rest_base = 'category';
138
- $wp_taxonomies['category']->rest_controller_class = 'WP_REST_Terms_Controller';
 
 
139
 
140
- $wp_taxonomies['post_tag']->show_in_rest = true;
141
- $wp_taxonomies['post_tag']->rest_base = 'tag';
142
- $wp_taxonomies['post_tag']->rest_controller_class = 'WP_REST_Terms_Controller';
 
 
143
  }
144
- add_action( 'init', '_add_extra_api_taxonomy_arguments', 11 );
145
 
146
  /**
147
- * Register default REST API routes
 
 
148
  */
149
  function create_initial_rest_routes() {
150
 
@@ -181,27 +179,19 @@ function create_initial_rest_routes() {
181
  }
182
  }
183
 
184
- /*
185
- * Post types
186
- */
187
  $controller = new WP_REST_Post_Types_Controller;
188
  $controller->register_routes();
189
 
190
- /*
191
- * Post statuses
192
- */
193
  $controller = new WP_REST_Post_Statuses_Controller;
194
  $controller->register_routes();
195
 
196
- /*
197
- * Taxonomies
198
- */
199
  $controller = new WP_REST_Taxonomies_Controller;
200
  $controller->register_routes();
201
 
202
- /*
203
- * Terms
204
- */
205
  foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
206
  $class = ! empty( $taxonomy->rest_controller_class ) ? $taxonomy->rest_controller_class : 'WP_REST_Terms_Controller';
207
 
@@ -216,492 +206,74 @@ function create_initial_rest_routes() {
216
  $controller->register_routes();
217
  }
218
 
219
- /*
220
- * Users
221
- */
222
  $controller = new WP_REST_Users_Controller;
223
  $controller->register_routes();
224
 
225
- /**
226
- * Comments
227
- */
228
  $controller = new WP_REST_Comments_Controller;
229
  $controller->register_routes();
230
-
231
- }
232
- add_action( 'rest_api_init', 'create_initial_rest_routes', 0 );
233
-
234
- /**
235
- * Register rewrite rules for the API.
236
- *
237
- * @global WP $wp Current WordPress environment instance.
238
- */
239
- function rest_api_init() {
240
- rest_api_register_rewrites();
241
-
242
- global $wp;
243
- $wp->add_query_var( 'rest_route' );
244
- }
245
- add_action( 'init', 'rest_api_init' );
246
-
247
- /**
248
- * Add rewrite rules.
249
- */
250
- function rest_api_register_rewrites() {
251
- add_rewrite_rule( '^' . rest_get_url_prefix() . '/?$','index.php?rest_route=/','top' );
252
- add_rewrite_rule( '^' . rest_get_url_prefix() . '(.*)?','index.php?rest_route=$matches[1]','top' );
253
- }
254
-
255
- /**
256
- * Determine if the rewrite rules should be flushed.
257
- */
258
- function rest_api_maybe_flush_rewrites() {
259
- $version = get_option( 'rest_api_plugin_version', null );
260
-
261
- if ( empty( $version ) || REST_API_VERSION !== $version ) {
262
- flush_rewrite_rules();
263
- update_option( 'rest_api_plugin_version', REST_API_VERSION );
264
- }
265
-
266
- }
267
- add_action( 'init', 'rest_api_maybe_flush_rewrites', 999 );
268
-
269
- /**
270
- * Register the default REST API filters.
271
- *
272
- * @internal This will live in default-filters.php
273
- *
274
- * @global WP_REST_Posts $WP_REST_posts
275
- * @global WP_REST_Pages $WP_REST_pages
276
- * @global WP_REST_Media $WP_REST_media
277
- * @global WP_REST_Taxonomies $WP_REST_taxonomies
278
- *
279
- * @param WP_REST_Server $server Server object.
280
- */
281
- function rest_api_default_filters( $server ) {
282
- // Deprecated reporting.
283
- add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 );
284
- add_filter( 'deprecated_function_trigger_error', '__return_false' );
285
- add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 );
286
- add_filter( 'deprecated_argument_trigger_error', '__return_false' );
287
-
288
- // Default serving
289
- add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
290
- add_filter( 'rest_post_dispatch', 'rest_send_allow_header', 10, 3 );
291
-
292
- add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 );
293
-
294
  }
295
- add_action( 'rest_api_init', 'rest_api_default_filters', 10, 1 );
296
-
297
- /**
298
- * Load the REST API.
299
- *
300
- * @todo Extract code that should be unit tested into isolated methods such as
301
- * the wp_rest_server_class filter and serving requests. This would also
302
- * help for code re-use by `wp-json` endpoint. Note that we can't unit
303
- * test any method that calls die().
304
- */
305
- function rest_api_loaded() {
306
- if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
307
- return;
308
- }
309
 
 
310
  /**
311
- * Whether this is a XML-RPC Request.
312
  *
313
- * @var bool
314
- * @todo Remove me in favour of REST_REQUEST
315
  */
316
- define( 'XMLRPC_REQUEST', true );
317
-
318
- /**
319
- * Whether this is a REST Request.
320
- *
321
- * @var bool
322
- */
323
- define( 'REST_REQUEST', true );
324
-
325
- /** @var WP_REST_Server $wp_rest_server */
326
- global $wp_rest_server;
327
-
328
- // Allow for a plugin to insert a different class to handle requests.
329
- $wp_rest_server_class = apply_filters( 'wp_rest_server_class', 'WP_REST_Server' );
330
- $wp_rest_server = new $wp_rest_server_class;
331
-
332
- /**
333
- * Fires when preparing to serve an API request.
334
- *
335
- * Endpoint objects should be created and register their hooks on this
336
- * action rather than another action to ensure they're only loaded when
337
- * needed.
338
- *
339
- * @param WP_REST_Server $wp_rest_server Server object.
340
- */
341
- do_action( 'rest_api_init', $wp_rest_server );
342
-
343
- // Fire off the request.
344
- $wp_rest_server->serve_request( $GLOBALS['wp']->query_vars['rest_route'] );
345
-
346
- // We're done.
347
- die();
348
- }
349
- add_action( 'parse_request', 'rest_api_loaded' );
350
-
351
- /**
352
- * Register routes and flush the rewrite rules on activation.
353
- *
354
- * @param bool $network_wide ?
355
- */
356
- function rest_api_activation( $network_wide ) {
357
- if ( function_exists( 'is_multisite' ) && is_multisite() && $network_wide ) {
358
- $mu_blogs = wp_get_sites();
359
-
360
- foreach ( $mu_blogs as $mu_blog ) {
361
- switch_to_blog( $mu_blog['blog_id'] );
362
-
363
- rest_api_register_rewrites();
364
- update_option( 'rest_api_plugin_version', null );
365
- }
366
-
367
- restore_current_blog();
368
- } else {
369
- rest_api_register_rewrites();
370
- update_option( 'rest_api_plugin_version', null );
371
  }
372
  }
373
- register_activation_hook( __FILE__, 'rest_api_activation' );
374
 
375
- /**
376
- * Flush the rewrite rules on deactivation.
377
- *
378
- * @param bool $network_wide ?
379
- */
380
- function rest_api_deactivation( $network_wide ) {
381
- if ( function_exists( 'is_multisite' ) && is_multisite() && $network_wide ) {
382
-
383
- $mu_blogs = wp_get_sites();
384
-
385
- foreach ( $mu_blogs as $mu_blog ) {
386
- switch_to_blog( $mu_blog['blog_id'] );
387
- delete_option( 'rest_api_plugin_version' );
388
- }
389
-
390
- restore_current_blog();
391
- } else {
392
- delete_option( 'rest_api_plugin_version' );
393
- }
394
- }
395
- register_deactivation_hook( __FILE__, 'rest_api_deactivation' );
396
-
397
- /**
398
- * Get the URL prefix for any API resource.
399
- *
400
- * @return string Prefix.
401
- */
402
- function rest_get_url_prefix() {
403
  /**
404
- * Filter the rest URL prefix.
405
  *
406
- * @since 1.0
 
407
  *
408
- * @param string $prefix URL prefix. Default 'wp-json'.
409
- */
410
- return apply_filters( 'rest_url_prefix', 'wp-json' );
411
- }
412
-
413
- /**
414
- * Get URL to a REST endpoint on a site.
415
- *
416
- * @todo Check if this is even necessary
417
- *
418
- * @param int $blog_id Blog ID. Optional. The ID of the multisite blog to get URL for. Default null of null returns URL for current blog.
419
- * @param string $path Optional. REST route. Default empty.
420
- * @param string $scheme Optional. Sanitization scheme. Default 'json'.
421
- * @return string Full URL to the endpoint.
422
- */
423
- function get_rest_url( $blog_id = null, $path = '', $scheme = 'json' ) {
424
- if ( get_option( 'permalink_structure' ) ) {
425
- $url = get_home_url( $blog_id, rest_get_url_prefix(), $scheme );
426
-
427
- if ( ! empty( $path ) && is_string( $path ) && strpos( $path, '..' ) === false ) {
428
- $url .= '/' . ltrim( $path, '/' );
429
- }
430
- } else {
431
- $url = trailingslashit( get_home_url( $blog_id, '', $scheme ) );
432
-
433
- if ( empty( $path ) ) {
434
- $path = '/';
435
- } else {
436
- $path = '/' . ltrim( $path, '/' );
437
- }
438
-
439
- $url = add_query_arg( 'rest_route', $path, $url );
440
- }
441
-
442
- /**
443
- * Filter the REST URL.
444
  *
445
- * @since 1.0
446
- *
447
- * @param string $url REST URL.
448
- * @param string $path REST route.
449
- * @param int $blod_ig Blog ID.
450
- * @param string $scheme Sanitization scheme.
 
 
 
451
  */
452
- return apply_filters( 'rest_url', $url, $path, $blog_id, $scheme );
453
- }
454
-
455
- /**
456
- * Get URL to a REST endpoint.
457
- *
458
- * @param string $path Optional. REST route. Default empty.
459
- * @param string $scheme Optional. Sanitization scheme. Default 'json'.
460
- * @return string Full URL to the endpoint.
461
- */
462
- function rest_url( $path = '', $scheme = 'json' ) {
463
- return get_rest_url( null, $path, $scheme );
464
- }
465
-
466
- /**
467
- * Do a REST request.
468
- * Used primarily to route internal requests through WP_REST_Server
469
- *
470
- * @param WP_REST_Request|string $request
471
- * @return WP_REST_Response
472
- */
473
- function rest_do_request( $request ) {
474
- global $wp_rest_server;
475
- $request = rest_ensure_request( $request );
476
- return $wp_rest_server->dispatch( $request );
477
- }
478
-
479
- /**
480
- * Ensure request arguments are a request object.
481
- *
482
- * This ensures that the request is consistent.
483
- *
484
- * @param array|WP_REST_Request $request Request to check.
485
- * @return WP_REST_Request
486
- */
487
- function rest_ensure_request( $request ) {
488
- if ( $request instanceof WP_REST_Request ) {
489
- return $request;
490
- }
491
-
492
- return new WP_REST_Request( 'GET', '', $request );
493
- }
494
-
495
- /**
496
- * Ensure a REST response is a response object.
497
- *
498
- * This ensures that the response is consistent, and implements
499
- * {@see WP_HTTP_ResponseInterface}, allowing usage of
500
- * `set_status`/`header`/etc without needing to double-check the object. Will
501
- * also allow {@see WP_Error} to indicate error responses, so users should
502
- * immediately check for this value.
503
- *
504
- * @param WP_Error|WP_HTTP_ResponseInterface|mixed $response Response to check.
505
- * @return mixed WP_Error if present, WP_HTTP_ResponseInterface if instance,
506
- * otherwise WP_REST_Response.
507
- */
508
- function rest_ensure_response( $response ) {
509
- if ( is_wp_error( $response ) ) {
510
- return $response;
511
- }
512
-
513
- if ( $response instanceof WP_HTTP_ResponseInterface ) {
514
- return $response;
515
- }
516
-
517
- return new WP_REST_Response( $response );
518
- }
519
-
520
- /**
521
- * Handle {@see _deprecated_function()} errors.
522
- *
523
- * @param string $function Function name.
524
- * @param string $replacement Replacement function name.
525
- * @param string $version Version.
526
- */
527
- function rest_handle_deprecated_function( $function, $replacement, $version ) {
528
- if ( ! empty( $replacement ) ) {
529
- $string = sprintf( __( '%1$s (since %2$s; use %3$s instead)' ), $function, $version, $replacement );
530
- } else {
531
- $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
532
- }
533
 
534
- header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) );
535
- }
536
-
537
- /**
538
- * Handle {@see _deprecated_function} errors.
539
- *
540
- * @param string $function Function name.
541
- * @param string $replacement Replacement function name.
542
- * @param string $version Version.
543
- */
544
- function rest_handle_deprecated_argument( $function, $replacement, $version ) {
545
- if ( ! empty( $replacement ) ) {
546
- $string = sprintf( __( '%1$s (since %2$s; %3$s)' ), $function, $version, $replacement );
547
- } else {
548
- $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
549
- }
550
-
551
- header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) );
552
- }
553
-
554
- /**
555
- * Send Cross-Origin Resource Sharing headers with API requests
556
- *
557
- * @param mixed $value Response data
558
- * @return mixed Response data
559
- */
560
- function rest_send_cors_headers( $value ) {
561
- $origin = get_http_origin();
562
-
563
- if ( $origin ) {
564
- header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
565
- header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE' );
566
- header( 'Access-Control-Allow-Credentials: true' );
567
- }
568
-
569
- return $value;
570
- }
571
-
572
- /**
573
- * Handle OPTIONS requests for the server
574
- *
575
- * This is handled outside of the server code, as it doesn't obey normal route
576
- * mapping.
577
- *
578
- * @param mixed $response Current response, either response or `null` to indicate pass-through.
579
- * @param WP_REST_Server $handler ResponseHandler instance (usually WP_REST_Server).
580
- * @param WP_REST_Request $request The request that was used to make current response.
581
- * @return WP_REST_Response $response Modified response, either response or `null` to indicate pass-through.
582
- */
583
- function rest_handle_options_request( $response, $handler, $request ) {
584
- if ( ! empty( $response ) || $request->get_method() !== 'OPTIONS' ) {
585
- return $response;
586
- }
587
-
588
- $response = new WP_REST_Response();
589
-
590
- $accept = array();
591
-
592
- foreach ( $handler->get_routes() as $route => $endpoints ) {
593
- $match = preg_match( '@^' . $route . '$@i', $request->get_route(), $args );
594
-
595
- if ( ! $match ) {
596
- continue;
597
- }
598
-
599
- foreach ( $endpoints as $endpoint ) {
600
- $accept = array_merge( $accept, $endpoint['methods'] );
601
- }
602
- break;
603
- }
604
- $accept = array_keys( $accept );
605
-
606
- $response->header( 'Accept', implode( ', ', $accept ) );
607
-
608
- return $response;
609
- }
610
-
611
- /**
612
- * Send the "Allow" header to state all methods that can be sen
613
- * to the current route
614
- *
615
- * @param WP_REST_Response $response Current response being served.
616
- * @param WP_REST_Server $server ResponseHandler instance (usually WP_REST_Server)
617
- * @param WP_REST_Request $request The request that was used to make current response.
618
- */
619
- function rest_send_allow_header( $response, $server, $request ) {
620
-
621
- $matched_route = $response->get_matched_route();
622
-
623
- if ( ! $matched_route ) {
624
- return $response;
625
- }
626
-
627
- $routes = $server->get_routes();
628
 
629
- $allowed_methods = array();
630
 
631
- // get the allowed methods across the routes
632
- foreach ( $routes[ $matched_route ] as $_handler ) {
633
- foreach ( $_handler['methods'] as $handler_method => $value ) {
634
 
635
- if ( ! empty( $_handler['permission_callback'] ) ) {
636
-
637
- $permission = call_user_func( $_handler['permission_callback'], $request );
638
-
639
- $allowed_methods[ $handler_method ] = true === $permission;
640
- } else {
641
- $allowed_methods[ $handler_method ] = true;
642
- }
643
  }
644
  }
645
-
646
- // strip out all the methods that are not allowed (false values)
647
- $allowed_methods = array_filter( $allowed_methods );
648
-
649
- if ( $allowed_methods ) {
650
- $response->header( 'Allow', implode( ', ', array_map( 'strtoupper', array_keys( $allowed_methods ) ) ) );
651
- }
652
-
653
- return $response;
654
  }
655
 
656
- if ( ! function_exists( 'json_last_error_msg' ) ) :
657
  /**
658
- * Returns the error string of the last json_encode() or json_decode() call
659
- *
660
- * @internal This is a compatibility function for PHP <5.5
661
- *
662
- * @return boolean|string Returns the error message on success, "No Error" if no error has occurred, or FALSE on failure.
663
  */
664
- function json_last_error_msg() {
665
- // see https://core.trac.wordpress.org/ticket/27799
666
- if ( ! function_exists( 'json_last_error' ) ) {
667
- return false;
668
- }
669
-
670
- $last_error_code = json_last_error();
671
-
672
- // just in case JSON_ERROR_NONE is not defined
673
- $error_code_none = defined( 'JSON_ERROR_NONE' ) ? JSON_ERROR_NONE : 0;
674
-
675
- switch ( true ) {
676
- case $last_error_code === $error_code_none:
677
- return 'No error';
678
-
679
- case defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code:
680
- return 'Maximum stack depth exceeded';
681
-
682
- case defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code:
683
- return 'State mismatch (invalid or malformed JSON)';
684
-
685
- case defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code:
686
- return 'Control character error, possibly incorrectly encoded';
687
-
688
- case defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code:
689
- return 'Syntax error';
690
-
691
- case defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code:
692
- return 'Malformed UTF-8 characters, possibly incorrectly encoded';
693
-
694
- case defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code:
695
- return 'Recursion detected';
696
-
697
- case defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code:
698
- return 'Inf and NaN cannot be JSON encoded';
699
-
700
- case defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code:
701
- return 'Type is not supported';
702
-
703
- default:
704
- return 'An unknown error occurred';
705
- }
706
  }
707
- endif;
4
  * Description: JSON-based REST API for WordPress, developed as part of GSoC 2013.
5
  * Author: WP REST API Team
6
  * Author URI: http://wp-api.org
7
+ * Version: 2.0-beta10
8
  * Plugin URI: https://github.com/WP-API/WP-API
9
  * License: GPL2+
10
  */
11
 
12
  /**
13
+ * WP_REST_Controller class.
 
 
14
  */
15
+ require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-controller.php';
16
 
17
  /**
18
+ * WP_REST_Posts_Controller class.
19
  */
 
 
 
 
 
 
 
 
 
 
 
20
  require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-posts-controller.php';
 
 
 
 
 
 
 
 
 
 
 
21
 
22
+ /**
23
+ * WP_REST_Attachments_Controller class.
24
+ */
25
+ require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-attachments-controller.php';
26
 
27
+ /**
28
+ * WP_REST_Post_Types_Controller class.
29
+ */
30
+ require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-post-types-controller.php';
31
 
32
  /**
33
+ * WP_REST_Post_Statuses_Controller class.
 
 
 
 
 
34
  */
35
+ require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-post-statuses-controller.php';
36
 
37
+ /**
38
+ * WP_REST_Revisions_Controller class.
39
+ */
40
+ require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-revisions-controller.php';
41
 
42
+ /**
43
+ * WP_REST_Taxonomies_Controller class.
44
+ */
45
+ require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-taxonomies-controller.php';
46
 
47
+ /**
48
+ * WP_REST_Terms_Controller class.
49
+ */
50
+ require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-terms-controller.php';
 
 
 
 
51
 
52
+ /**
53
+ * WP_REST_Users_Controller class.
54
+ */
55
+ require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-users-controller.php';
56
 
57
  /**
58
+ * WP_REST_Comments_Controller class.
 
 
 
 
 
59
  */
60
+ require_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-comments-controller.php';
61
 
62
+ /**
63
+ * WP_REST_Meta_Controller class.
64
+ */
65
+ include_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-meta-controller.php';
 
66
 
67
+ /**
68
+ * WP_REST_Meta_Posts_Controller class.
69
+ */
70
+ include_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-meta-posts-controller.php';
71
 
72
+ /**
73
+ * WP_REST_Posts_Terms_Controller class.
74
+ */
75
+ include_once dirname( __FILE__ ) . '/lib/endpoints/class-wp-rest-posts-terms-controller.php';
76
 
77
+ /**
78
+ * REST extras.
79
+ */
80
+ include_once( dirname( __FILE__ ) . '/extras.php' );
81
 
82
+ add_filter( 'init', '_add_extra_api_post_type_arguments', 11 );
83
+ add_action( 'init', '_add_extra_api_taxonomy_arguments', 11 );
84
+ add_action( 'rest_api_init', 'create_initial_rest_routes', 0 );
 
85
 
86
  /**
87
+ * Adds extra post type registration arguments.
88
+ *
89
  * These attributes will eventually be committed to core.
90
+ *
91
+ * @since 4.4.0
92
+ *
93
+ * @global array $wp_taxonomies Registered taxonomies.
94
  */
95
  function _add_extra_api_post_type_arguments() {
96
  global $wp_post_types;
97
 
98
+ if ( isset( $wp_post_types['post'] ) ) {
99
+ $wp_post_types['post']->show_in_rest = true;
100
+ $wp_post_types['post']->rest_base = 'posts';
101
+ $wp_post_types['post']->rest_controller_class = 'WP_REST_Posts_Controller';
102
+ }
 
 
103
 
104
+ if ( isset( $wp_post_types['page'] ) ) {
105
+ $wp_post_types['page']->show_in_rest = true;
106
+ $wp_post_types['page']->rest_base = 'pages';
107
+ $wp_post_types['page']->rest_controller_class = 'WP_REST_Posts_Controller';
108
+ }
109
 
110
+ if ( isset( $wp_post_types['attachment'] ) ) {
111
+ $wp_post_types['attachment']->show_in_rest = true;
112
+ $wp_post_types['attachment']->rest_base = 'media';
113
+ $wp_post_types['attachment']->rest_controller_class = 'WP_REST_Attachments_Controller';
114
+ }
115
  }
 
116
 
117
  /**
118
+ * Adds extra taxonomy registration arguments.
119
+ *
120
  * These attributes will eventually be committed to core.
121
+ *
122
+ * @since 4.4.0
123
+ *
124
+ * @global array $wp_taxonomies Registered taxonomies.
125
  */
126
  function _add_extra_api_taxonomy_arguments() {
127
  global $wp_taxonomies;
128
 
129
+ if ( isset( $wp_taxonomies['category'] ) ) {
130
+ $wp_taxonomies['category']->show_in_rest = true;
131
+ $wp_taxonomies['category']->rest_base = 'categories';
132
+ $wp_taxonomies['category']->rest_controller_class = 'WP_REST_Terms_Controller';
133
+ }
134
 
135
+ if ( isset( $wp_taxonomies['post_tag'] ) ) {
136
+ $wp_taxonomies['post_tag']->show_in_rest = true;
137
+ $wp_taxonomies['post_tag']->rest_base = 'tags';
138
+ $wp_taxonomies['post_tag']->rest_controller_class = 'WP_REST_Terms_Controller';
139
+ }
140
  }
 
141
 
142
  /**
143
+ * Registers default REST API routes.
144
+ *
145
+ * @since 4.4.0
146
  */
147
  function create_initial_rest_routes() {
148
 
179
  }
180
  }
181
 
182
+ // Post types.
 
 
183
  $controller = new WP_REST_Post_Types_Controller;
184
  $controller->register_routes();
185
 
186
+ // Post statuses.
 
 
187
  $controller = new WP_REST_Post_Statuses_Controller;
188
  $controller->register_routes();
189
 
190
+ // Taxonomies.
 
 
191
  $controller = new WP_REST_Taxonomies_Controller;
192
  $controller->register_routes();
193
 
194
+ // Terms.
 
 
195
  foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
196
  $class = ! empty( $taxonomy->rest_controller_class ) ? $taxonomy->rest_controller_class : 'WP_REST_Terms_Controller';
197
 
206
  $controller->register_routes();
207
  }
208
 
209
+ // Users.
 
 
210
  $controller = new WP_REST_Users_Controller;
211
  $controller->register_routes();
212
 
213
+ // Comments.
 
 
214
  $controller = new WP_REST_Comments_Controller;
215
  $controller->register_routes();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
+ if ( ! function_exists( 'rest_authorization_required_code' ) ) {
219
  /**
220
+ * Returns a contextual HTTP error code for authorization failure.
221
  *
222
+ * @return integer
 
223
  */
224
+ function rest_authorization_required_code() {
225
+ return is_user_logged_in() ? 403 : 401;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  }
227
  }
 
228
 
229
+ if ( ! function_exists( 'register_rest_field' ) ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  /**
231
+ * Registers a new field on an existing WordPress object type.
232
  *
233
+ * @global array $wp_rest_additional_fields Holds registered fields, organized
234
+ * by object type.
235
  *
236
+ * @param string|array $object_type Object(s) the field is being registered
237
+ * to, "post"|"term"|"comment" etc.
238
+ * @param string $attribute The attribute name.
239
+ * @param array $args {
240
+ * Optional. An array of arguments used to handle the registered field.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  *
242
+ * @type string|array|null $get_callback Optional. The callback function used to retrieve the field
243
+ * value. Default is 'null', the field will not be returned in
244
+ * the response.
245
+ * @type string|array|null $update_callback Optional. The callback function used to set and update the
246
+ * field value. Default is 'null', the value cannot be set or
247
+ * updated.
248
+ * @type string|array|null $schema Optional. The callback function used to create the schema for
249
+ * this field. Default is 'null', no schema entry will be returned.
250
+ * }
251
  */
252
+ function register_rest_field( $object_type, $attribute, $args = array() ) {
253
+ $defaults = array(
254
+ 'get_callback' => null,
255
+ 'update_callback' => null,
256
+ 'schema' => null,
257
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
+ $args = wp_parse_args( $args, $defaults );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
+ global $wp_rest_additional_fields;
262
 
263
+ $object_types = (array) $object_type;
 
 
264
 
265
+ foreach ( $object_types as $object_type ) {
266
+ $wp_rest_additional_fields[ $object_type ][ $attribute ] = $args;
 
 
 
 
 
 
267
  }
268
  }
 
 
 
 
 
 
 
 
 
269
  }
270
 
271
+ if ( ! function_exists( 'register_api_field' ) ) {
272
  /**
273
+ * Backwards compat shim
 
 
 
 
274
  */
275
+ function register_api_field( $object_type, $attributes, $args = array() ) {
276
+ _deprecated_function( 'register_api_field', 'WPAPI-2.0', 'register_rest_field' );
277
+ register_rest_field( $object_type, $attributes, $args );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  }
279
+ }
readme.txt CHANGED
@@ -1,9 +1,9 @@
1
  === WordPress REST API (Version 2) ===
2
- Contributors: rmccue, rachelbaker
3
  Tags: json, rest, api, rest-api
4
- Requires at least: 4.3-alpha
5
- Tested up to: 4.3-beta
6
- Stable tag: 2.0-beta3.1
7
  License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
@@ -36,13 +36,968 @@ For full-flavoured API support, you'll need to be using pretty permalinks to use
36
 
37
  == Changelog ==
38
 
39
- = 2.0 Beta 3.1 =
40
 
41
- * Ensure media of private posts are private too.
42
 
43
  Reported by @danielbachhuber on 2016-01-08.
44
 
45
- = Version 2.0 Beta 1 =
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  Partial rewrite and evolution of the REST API to prepare for core integration.
48
 
1
  === WordPress REST API (Version 2) ===
2
+ Contributors: rmccue, rachelbaker, danielbachhuber, joehoyle
3
  Tags: json, rest, api, rest-api
4
+ Requires at least: 4.4
5
+ Tested up to: 4.5-alpha
6
+ Stable tag: 2.0-beta10
7
  License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
36
 
37
  == Changelog ==
38
 
39
+ = 2.0 Beta 10.0 =
40
 
41
+ * SECURITY: Ensure media of private posts are private too.
42
 
43
  Reported by @danielbachhuber on 2016-01-08.
44
 
45
+ * BREAKING CHANGE: Removes compatibility repo for WordPress 4.3.
46
+
47
+ WordPress 4.4 is now the minimum supported WordPress version.
48
+
49
+ (props @danielbachhuber, [#1848](https://github.com/WP-API/WP-API/pull/1848))
50
+
51
+ * BREAKING CHANGE: Changes link relation for types and taxonomies.
52
+
53
+ In Beta 9, this link relation was introduced as `item`, which isn't correct. The relation has been changed to `https://api.w.org/items`.
54
+
55
+ (props @danielbachhuber, [#1853](https://github.com/WP-API/WP-API/pull/1853))
56
+
57
+ * BREAKING CHANGE: Introduces `edit` context for `wp/v2/types` and `wp/v2/taxonomies`.
58
+
59
+ Some fields have moved into this context, which require `edit_posts` and `manage_terms`, respectively.
60
+
61
+ (props @danielbachhuber, [#1894](https://github.com/WP-API/WP-API/pull/1894), [#1864](https://github.com/WP-API/WP-API/pull/1864))
62
+
63
+ * BREAKING CHANGE: Removes `post_format` as a term `_link` for Posts.
64
+
65
+ Post formats aren't a custom taxonomy in the eyes of the REST API.
66
+
67
+ (props @danielbachhuber, [#1854](https://github.com/WP-API/WP-API/pull/1854))
68
+
69
+ * Declares `parent` query param for Pages.
70
+
71
+ (props @danielbachhuber, [#1975](https://github.com/WP-API/WP-API/pull/1975))
72
+
73
+ * Permits logged-in users to query for media.
74
+
75
+ (props @danielbachhuber, [#1973](https://github.com/WP-API/WP-API/pull/1973))
76
+
77
+ * Removes duplicated query params from Terms controller.
78
+
79
+ (props @danielbachhuber, [#1963](https://github.com/WP-API/WP-API/pull/1963))
80
+
81
+ * Adds `include` param to `/wp/v2/posts`, `/wp/v2/users`, `/wp/v2/<taxonomy>` and `/wp/v2/comments`.
82
+
83
+ (props @danielbachhuber, [#1961](https://github.com/WP-API/WP-API/pull/1961), [#1964](https://github.com/WP-API/WP-API/pull/1964), [#1968](https://github.com/WP-API/WP-API/pull/1968), [#1971](https://github.com/WP-API/WP-API/pull/1971))
84
+
85
+ * Ensures `GET /wp/v2/posts` respects `order` and `orderby` params.
86
+
87
+ (props @danielbachhuber, [#1962](https://github.com/WP-API/WP-API/pull/1962))
88
+
89
+ * Fixes fatal by loading `wp-admin/includes/user.php` to expose `wp_delete_user()`.
90
+
91
+ (props @danielbachhuber, [#1958](https://github.com/WP-API/WP-API/pull/1958))
92
+
93
+ * Permits making a post sticky when also supplying an empty password.
94
+
95
+ (props @westonruter, [#1949](https://github.com/WP-API/WP-API/pull/1949))
96
+
97
+ * Uses `WP_REST_Request` internally across controllers.
98
+
99
+ (props @danielbachhuber, [#1933](https://github.com/WP-API/WP-API/pull/1933), [#1939](https://github.com/WP-API/WP-API/pull/1939), [#1934](https://github.com/WP-API/WP-API/pull/1934), [#1938](https://github.com/WP-API/WP-API/pull/1938))
100
+
101
+ * Cleans up permissions checks in `WP_REST_Terms_Controller`.
102
+
103
+ (props @danielbachhuber, [#1941](https://github.com/WP-API/WP-API/pull/1941))
104
+
105
+ * Uses `show_in_rest` to determine publicness for post types.
106
+
107
+ (props @danielbachhuber, [#1942](https://github.com/WP-API/WP-API/pull/1942))
108
+
109
+ * Makes `description` strings available for translation.
110
+
111
+ (props @danielbachhuber, [#1944](https://github.com/WP-API/WP-API/pull/1944))
112
+
113
+ * Checks `assign_terms` cap for taxonomy when managing post terms.
114
+
115
+ (props @danielbachhuber, [#1940](https://github.com/WP-API/WP-API/pull/1940))
116
+
117
+ * Defer to `edit_posts` of the custom post type when accessing private query vars.
118
+
119
+ (props @danielbachhuber, [#1886](https://github.com/WP-API/WP-API/pull/1886))
120
+
121
+ * Allows Terms collection params to be filtered.
122
+
123
+ (props @rachelbaker, [#1882](https://github.com/WP-API/WP-API/pull/1882))
124
+
125
+ * Renames post terms create/delete permissions callback.
126
+
127
+ (props @wpsmith, [#1923](https://github.com/WP-API/WP-API/pull/1923))
128
+
129
+ * Fixes invalid use of 'uri' as schema `type`.
130
+
131
+ (props @wpsmith, [#1913](https://github.com/WP-API/WP-API/pull/1913))
132
+
133
+ * Casts integer with (int) over intval for speed.
134
+
135
+ (props @wpsmith, [#1907](https://github.com/WP-API/WP-API/pull/1907))
136
+
137
+ * Fixes PHP Doc typo for `validate_schema_property` and `sanitize_schema_property`.
138
+
139
+ (props @wpsmith, @danielbachhuber, [#1909](https://github.com/WP-API/WP-API/pull/1909), [#1910](https://github.com/WP-API/WP-API/pull/1910))
140
+
141
+ * Adds a helpful description to the `filter` argument.
142
+
143
+ (props @danielbachhuber, [#1885](https://github.com/WP-API/WP-API/pull/1885))
144
+
145
+ * Changes order of Users response to match schema order.
146
+
147
+ (props @rachelbaker, [#1879](https://github.com/WP-API/WP-API/pull/1879))
148
+
149
+ * Adjusts Posts pagination headers for `filter` params.
150
+
151
+ (props @rachelbaker, [#1878](https://github.com/WP-API/WP-API/pull/1878))
152
+
153
+ * Uses proper status code when failing to get comments of private post.
154
+
155
+ (props @danielbachhuber, [#1866](https://github.com/WP-API/WP-API/pull/1867))
156
+
157
+ * Fixes invalid capability for comments get items permissions callback.
158
+
159
+ `manage_comments` doesn't exist; `moderate_comments` does.
160
+
161
+ (props @danielbachhuber, [#1866](https://github.com/WP-API/WP-API/pull/1866))
162
+
163
+ * Permits creating comments without an assigned post.
164
+
165
+ (props @danielbachhuber, [#1857](https://github.com/WP-API/WP-API/pull/1857))
166
+
167
+ * Prevents error notice when `show_in_rest` isn't set for a post type.
168
+
169
+ (props @danielbachhuber, [#1852](https://github.com/WP-API/WP-API/pull/1852))
170
+
171
+ = 2.0 Beta 9.0 =
172
+
173
+ * BREAKING CHANGE: Move tags and categories to top-level endpoints.
174
+
175
+ Tags are now accessible at `/wp/v2/tags`, and categories accessible at `/wp/v2/categories`. Post terms reside at `/wp/v2/posts/<id>/tags` and `/wp/v2/<id>/categories`.
176
+
177
+ (props @danielbachhuber, [#1802](https://github.com/WP-API/WP-API/pull/1802))
178
+
179
+ * BREAKING CHANGE: Return object for requests to `/wp/v2/taxonomies`.
180
+
181
+ This is consistent with `/wp/v2/types` and `/wp/v2/statuses`.
182
+
183
+ (props @danielbachhuber, [#1825](https://github.com/WP-API/WP-API/pull/1825))
184
+
185
+ * BREAKING CHANGE: Remove `rest_get_timezone()`.
186
+
187
+ `json_get_timezone()` was only ever used in v1. This function causes fatals, and shouldn't be used.
188
+
189
+ (props @danielbachhuber, [#1823](https://github.com/WP-API/WP-API/pull/1823))
190
+
191
+ * BREAKING CHANGE: Rename `register_api_field()` to `register_rest_field()`.
192
+
193
+ Introduces a `register_api_field()` function for backwards compat, which calls `_doing_it_wrong()`. However, `register_api_field()` won't ever be committed to WordPress core, so you should update your function calls.
194
+
195
+ (props @danielbachhuber, [#1824](https://github.com/WP-API/WP-API/pull/1824))
196
+
197
+ * BREAKING CHANGE: Change taxonomies' `post_type` argument to `type`.
198
+
199
+ It's consistent with how we're exposing post types in the API.
200
+
201
+ (props @danielbachhuber, [#1824](https://github.com/WP-API/WP-API/pull/1824))
202
+
203
+ * Sync infrastructure with shipped in WordPress 4.4.
204
+
205
+ * `wp-includes/rest-api/rest-functions.php` is removed, and its functions moved into `wp-includes/rest-api.php`.
206
+ * Send nocache headers for REST requests. [#34832](https://core.trac.wordpress.org/ticket/34832)
207
+ * Fix handling of HEAD requests. [#34837](https://core.trac.wordpress.org/ticket/34837)
208
+ * Mark `WP_REST_Server::get_raw_data()` as static. [#34768](https://core.trac.wordpress.org/ticket/34768)
209
+ * Unabbreviate error string. [#34818](https://core.trac.wordpress.org/ticket/34818)
210
+
211
+ * Change terms endpoints to use `term_id` not `tt_id`.
212
+
213
+ (props @joehoyle, [#1837](https://github.com/WP-API/WP-API/pull/1837))
214
+
215
+ * Standardize declaration of `context` param for `GET` requests across controllers.
216
+
217
+ However, we're still inconsistent in which controllers expose which params. Follow [#1845](https://github.com/WP-API/WP-API/issues/1845) for further discussion.
218
+
219
+ (props @danielbachhuber, [#1795](https://github.com/WP-API/WP-API/pull/1795), [#1835](https://github.com/WP-API/WP-API/pull/1835), [#1838](https://github.com/WP-API/WP-API/pull/1838))
220
+
221
+ * Link types / taxonomies to their collections, and vice versa.
222
+
223
+ Collections link to their type / taxonomy with the `about` relation; types / taxonomies link to their colletion with the `item` relation, which is imperfect and may change in the future.
224
+
225
+ (props @danielbachhuber, [#1814](https://github.com/WP-API/WP-API/pull/1814), [#1817](https://github.com/WP-API/WP-API/pull/1817), [#1829](https://github.com/WP-API/WP-API/pull/1829). [#1846](https://github.com/WP-API/WP-API/pull/1846))
226
+
227
+ * Add missing 'wp/v2' in Location Response header when creating new Post Meta.
228
+
229
+ (props @johanmynhardt, [#1790](https://github.com/WP-API/WP-API/pull/1790))
230
+
231
+ * Expose Post collection query params, including `author`, `order`, `orderby` and `status`.
232
+
233
+ (props @danielbachhuber, [#1793](https://github.com/WP-API/WP-API/pull/1793))
234
+
235
+ * Ignore sticky posts by default.
236
+
237
+ (props @danielbachhuber, [#1801](https://github.com/WP-API/WP-API/pull/1801))
238
+
239
+ * Include `full` image size in attachment `sizes` attribute.
240
+
241
+ (props @danielbachhuber, [#1806](https://github.com/WP-API/WP-API/pull/1806))
242
+
243
+ * In text strings, use `id` instead of `ID`.
244
+
245
+ `ID` is an implementation artifact. Our Resources use `id`.
246
+
247
+ (props @danielbachhuber, [#1803](https://github.com/WP-API/WP-API/pull/1803))
248
+
249
+ * Ensure `attachment.sizes[]` use `mime_type` instead of `mime-type`.
250
+
251
+ (props @danielbachhuber, [#1809](https://github.com/WP-API/WP-API/pull/1809))
252
+
253
+ * Introduce `rest_authorization_required_code()`.
254
+
255
+ Many controllers returned incorrect HTTP codes, which this also fixes.
256
+
257
+ (props @danielbachhuber, [#1808](https://github.com/WP-API/WP-API/pull/1808))
258
+
259
+ * Respect core's `comment_registration` setting.
260
+
261
+ If it's enabled, require users to be logged in to comment.
262
+
263
+ (props @danielbachhuber, [#1826](https://github.com/WP-API/WP-API/pull/1826))
264
+
265
+ * Default to wildcard when searching users.
266
+
267
+ (props @danielbachhuber, [#1827](https://github.com/WP-API/WP-API/pull/1827))
268
+
269
+ * Bring the wp-api.js library up to date for v2 of the REST API.
270
+
271
+ (props @adamsilverstein, [#1828](https://github.com/WP-API/WP-API/pull/1828))
272
+
273
+ * Add `rest_prepare_status` filter.
274
+
275
+ (props @danielbachhuber, [#1830](https://github.com/WP-API/WP-API/pull/1830))
276
+
277
+ * Make `prepare_*` filters more consistent.
278
+
279
+ (props @danielbachhuber, [#1831](https://github.com/WP-API/WP-API/pull/1831))
280
+
281
+ * Add `rest_prepare_post_type` filter for post types.
282
+
283
+ (props @danielbachhuber, [#1833](https://github.com/WP-API/WP-API/pull/1833))
284
+
285
+ = 2.0 Beta 8.0 =
286
+
287
+ * Prevent fatals when uploading attachment by including admin utilities.
288
+
289
+ (props @danielbachhuber, [#1756](https://github.com/WP-API/WP-API/pull/1756))
290
+
291
+ * Return 201 status code when creating a term.
292
+
293
+ (props @danielbachhuber, [#1753](https://github.com/WP-API/WP-API/pull/1753))
294
+
295
+ * Don't permit requesting terms cross routes.
296
+
297
+ Clients should only be able to request categories from the category route, and tags from the tag route.
298
+
299
+ (props @danielbachhuber, [#1764](https://github.com/WP-API/WP-API/pull/1764))
300
+
301
+ * Set `fields=>id` when using `WP_User_Query` to fix large memory usage
302
+
303
+ (props @joehoyle, [#1770](https://github.com/WP-API/WP-API/pull/1770))
304
+
305
+ * Fix Post `_link` to attached attachments.
306
+
307
+ (props @danielbachhuber, [#1777](https://github.com/WP-API/WP-API/pull/1777))
308
+
309
+ * Add support for getting a post with a custom public status.
310
+
311
+ (props @danielbachhuber, [#1765](https://github.com/WP-API/WP-API/pull/1765))
312
+
313
+ * Ensure post content doesn't get double-slashed on update.
314
+
315
+ (props @joehoyle, [#1772](https://github.com/WP-API/WP-API/pull/1772))
316
+
317
+ * Change 'int' to 'integer' for `WP_REST_Controller::validate_schema_property()`
318
+
319
+ (props @wpsmith, [#1759](https://github.com/WP-API/WP-API/pull/1759))
320
+
321
+ = 2.0 Beta 7.0 =
322
+
323
+ * Sync infrastructure from WordPress core as of r35691.
324
+
325
+ * Remove `register_api_field()` because it's conceptually tied to `WP_REST_Controller` [#34730](https://core.trac.wordpress.org/ticket/34730)
326
+ * Update the REST API header links to use api.w.org [#34303](https://core.trac.wordpress.org/ticket/34303)
327
+ * Require the `$namespace` argument in `register_rest_route()` [#34416](https://core.trac.wordpress.org/ticket/34416)
328
+ * Include `enum` and `description` in help data [#34543](https://core.trac.wordpress.org/ticket/34543)
329
+ * Save `preg_match` iterations in `WP_REST_Server` [#34488](https://core.trac.wordpress.org/ticket/34488)
330
+ * Don't return route URL in `WP_REST_Request:get_params()` [#34647](https://core.trac.wordpress.org/ticket/34647)
331
+
332
+ * Restore `register_api_field()` within the plugin.
333
+
334
+ (props @danielbachhuber, [#1748](https://github.com/WP-API/WP-API/pull/1748))
335
+
336
+ * Require admin functions for use of `wp_handle_upload()`, fixing fatal.
337
+
338
+ (props @joehoyle, [#1746](https://github.com/WP-API/WP-API/pull/1746))
339
+
340
+ * Properly handle requesting terms where `parent=0` and `0` is a string.
341
+
342
+ (props @danielbachhuber, [#1739](https://github.com/WP-API/WP-API/pull/1739))
343
+
344
+ * Prevent PHP error notice when `&filter` isn't an array.
345
+
346
+ (props @danielbachhuber, [#1734](https://github.com/WP-API/WP-API/pull/1734))
347
+
348
+ * Change link relations to use api.w.org.
349
+
350
+ (props @danielbachhuber, [#1726](https://github.com/WP-API/WP-API/pull/1726))
351
+
352
+ = 2.0 Beta 6.0 =
353
+
354
+ * Remove global inclusion of wp-admin/includes/admin.php
355
+
356
+ For a long time, the REST API loaded wp-admin/includes/admin.php to make use of specific admin utilities. Now, it only loads those admin utilities when it needs them.
357
+
358
+ If your custom endpoints make use of admin utilities, you'll need to make sure to load wp-admin/includes/admin.php before you use them.
359
+
360
+ (props @joehoyle, [#1696](https://github.com/WP-API/WP-API/pull/1696))
361
+
362
+ * Link directly to the featured image in a Post's links.
363
+
364
+ (props @rmccue, [#1563](https://github.com/WP-API/WP-API/pull/1563), [#1711](https://github.com/WP-API/WP-API/pull/1711))
365
+
366
+ * Provide object type as callback argument for custom API fields.
367
+
368
+ (props @jtsternberg, [#1714](https://github.com/WP-API/WP-API/pull/1714))
369
+
370
+ * Change users schema order to be order of importance instead of alpha.
371
+
372
+ (props @rachelbaker, [#1708](https://github.com/WP-API/WP-API/pull/1708))
373
+
374
+ * Clarify documentation for `date` and `modified` attributes.
375
+
376
+ (props @danielbachhuber, [#1715](https://github.com/WP-API/WP-API/pull/1715))
377
+
378
+ * Update the wp-api.js client from the client-js repo.
379
+
380
+ (props @rachelbaker, [#1709](https://github.com/WP-API/WP-API/pull/1709))
381
+
382
+ * Fix the `format` enum to be an array of strings.
383
+
384
+ (props @joehoyle, [#1707](https://github.com/WP-API/WP-API/pull/1707))
385
+
386
+ * Run revisions for collection through `prepare_response_for_collection()`.
387
+
388
+ (props @danielbachhuber, @rachelbaker, [#1671](https://github.com/WP-API/WP-API/pull/1671))
389
+
390
+ * Expose `date_gmt` for `view` context of Posts and Comments.
391
+
392
+ (props @danielbachhuber, [#1690](https://github.com/WP-API/WP-API/pull/1690))
393
+
394
+ * Fix PHP and JS docblock formatting.
395
+
396
+ (props @ahmadawais, [#1699](https://github.com/WP-API/WP-API/pull/1698), [#1699](https://github.com/WP-API/WP-API/pull/1699), [#1701](https://github.com/WP-API/WP-API/pull/1701), [#1700](https://github.com/WP-API/WP-API/pull/1700), [#1702](https://github.com/WP-API/WP-API/pull/1702), [#1703](https://github.com/WP-API/WP-API/pull/1703))
397
+
398
+ * Include `media_details` attribute for attachments in embed context.
399
+
400
+ For image attachments, media_details includes a sizes array of image sizes, which is useful for templating.
401
+
402
+ (props @danielbachhuber, [#1667](https://github.com/WP-API/WP-API/pull/1667))
403
+
404
+ * Make `WP_REST_Controller` error messages more helpful by specifying method to subclass.
405
+
406
+ (props @danielbachhuber, [#1670](https://github.com/WP-API/WP-API/pull/1670))
407
+
408
+ * Expose `slug` in `embed` context for Users.
409
+
410
+ `user_nicename` is a public attribute, used in user URLs, so this is safe data to present.
411
+
412
+ (props @danielbachhuber, [#1666](https://github.com/WP-API/WP-API/pull/1666))
413
+
414
+ * Handle falsy value from `wp_count_terms()`, fixing fatal.
415
+
416
+ (props @joehoyle, [#1641](https://github.com/WP-API/WP-API/pull/1641))
417
+
418
+ * Correct methods in `WP_REST_SERVER::EDITABLE` description.
419
+
420
+ (props @rachelbaker, [#1601](https://github.com/WP-API/WP-API/pull/1601))
421
+
422
+ * Add the embed context to Users collection query params.
423
+
424
+ (props @rachelbaker, [#1591](https://github.com/WP-API/WP-API/pull/1591))
425
+
426
+ * Add Terms Controller collection args details.
427
+
428
+ (props @rachelbaker, [#1603](https://github.com/WP-API/WP-API/pull/1603))
429
+
430
+ * Set comment author details from current user.
431
+
432
+ (props @rmccue, [#1580](https://github.com/WP-API/WP-API/pull/1580))
433
+
434
+ * More hook documentation.
435
+
436
+ (props @adamsilverstein, [#1556](https://github.com/WP-API/WP-API/pull/1556), [#1560](https://github.com/WP-API/WP-API/pull/1560))
437
+
438
+ * Return the trashed status of deleted posts/comments.
439
+
440
+ When a post or a comment is deleted, returns a flag to say whether it's been trashed or properly deleted.
441
+
442
+ (props @pento, [#1499](https://github.com/WP-API/WP-API/pull/1499))
443
+
444
+ * In `WP_REST_Posts_Controller::update_item()`, check the post ID based on the proper post type.
445
+
446
+ (props @rachelbaker, [#1497](https://github.com/WP-API/WP-API/pull/1497))
447
+
448
+ = 2.0 Beta 5.0 =
449
+
450
+ * Load api-core as a compatibility library
451
+
452
+ Now api-core has been merged into WordPress trunk (for 4.4) we should no longer load the infrastructure code when it's already available. This also fixes a fatal error for users who were on trunk.
453
+
454
+ (props @rmccue)
455
+
456
+ * Switch to new mysql_to_rfc3339
457
+
458
+ (props @rmccue)
459
+
460
+ * Double-check term taxonomy
461
+
462
+ (props @rmccue)
463
+
464
+ * Load admin functions
465
+
466
+ This was removed from the latest beta of WordPress in the REST API infrastructure, a more long term fix is planned.
467
+
468
+ (props @joehoyle)
469
+
470
+ * Add Add compat shim for renamed `rest_mysql_to_rfc3339()`
471
+
472
+ (props @danielbachhuber)
473
+
474
+ * Compat shim for `wp_is_numeric_array()`
475
+
476
+ (props @danielbachhuber)
477
+
478
+ * Revert Switch to register_post_type_args filter
479
+
480
+ (props @joehoyle)
481
+
482
+ = 2.0 Beta 4.0 =
483
+
484
+ * Show public user information through the user controller.
485
+
486
+ In WordPress as of [r32683](https://core.trac.wordpress.org/changeset/32683) (scheduled for 4.3), `WP_User_Query` now has support for getting users with published posts.
487
+
488
+ To match current behaviour in WordPress themes and feeds, we now expose this public user information. This includes the avatar, description, user ID, custom URL, display name, and URL, for users who have published at least one post on the site. This information is available to all clients; other fields and data for all users are still only available when authenticated.
489
+
490
+ (props @joehoyle, @rmccue, @Shelob9, [#1397][gh-1397], [#839][gh-839], [#1435][gh-1435])
491
+
492
+ * Send schema in OPTIONS requests and index.
493
+
494
+ Rather than using separate `/schema` endpoints, the schema for items is now available through an OPTIONS request to the route. This means that full documentation is now available for endpoints through an OPTIONS request; this includes available methods, what data you can pass to the endpoint, and the data you'll get back.
495
+
496
+ This data is now also available in the main index and namespace indexes. Simply request the index with `context=help` to get full schema data. Warning: this response will be huge. The schema for single endpoints is also available in the collection's OPTIONS response.
497
+
498
+ **⚠️ This breaks backwards compatibility** for clients relying on schemas being at their own routes. These clients should instead send `OPTIONS` requests.
499
+
500
+ Custom endpoints can register their own schema via the `schema` option on the route. This option should live side-by-side with the endpoints (similar to `relation` in WP's meta queries), so your registration call will look something like:
501
+
502
+ ```php
503
+ register_rest_route( 'test-ns', '/test', array(
504
+ array(
505
+ 'methods' => 'GET',
506
+ 'callback' => 'my_test_callback',
507
+ ),
508
+
509
+ 'schema' => 'my_schema_callback',
510
+ ) );
511
+ ```
512
+
513
+ (props @rmccue, [#1415][gh-1415], [#1222][gh-1222], [#1305][gh-1305])
514
+
515
+ * Update JavaScript API for version 2.
516
+
517
+ Our fantastic JavaScript API from version 1 is now available for version 2, refreshed with the latest and greatest changes.
518
+
519
+ As a refresher: if you want to use it, simply make your script depend on `wp-api` when you enqueue it. If you want to enqueue the script manually, add `wp_enqueue_script( 'wp-api' )` to a callback on `wp_enqueue_scripts`.
520
+
521
+ (props @tlovett1, @kadamwhite, @nathanrice, [#1374][gh-1374], [#1320][gh-1320])
522
+
523
+ * Embed links inside items in a collection.
524
+
525
+ Previously when fetching a collection of items, you only received the items themselves. To fetch the links as well via embedding, you needed to make a request to the single item with `_embed` set.
526
+
527
+ No longer! You can now request a collection with embeds enabled (try `/wp/v2/posts?_embed`). This will embed links inside each item, allowing you to build interface items much easier (for example, post archive pages can get featured image data at the same time).
528
+
529
+ This also applies to custom endpoints. Any endpoint that returns a list of objects will automatically have the embedding applied to objects inside the list.
530
+
531
+ (props @rmccue, [#1459][gh-1459], [#865][gh-865])
532
+
533
+ * Fix potential XSS vulnerability.
534
+
535
+ Requests from other origins could potentially run code on the API domain, allowing cross-origin access to authentication cookies or similar.
536
+
537
+ Reported by @xknown on 2015-07-23.
538
+
539
+ * Move `/posts` `WP_Query` vars back to `filter` param.
540
+
541
+ In version 1, we had internal `WP_Query` vars available via `filter` (e.g. `filter[s]=search+term`). For our first betas of version 2, we tried something different and exposed these directly on the endpoint. The experiment has now concluded; we didn't like this that much, so `filter` is back.
542
+
543
+ We plan on adding nicer looking arguments to collections in future releases, with a view towards being consistent across different collections. We also plan on opening up the underlying query vars via `filter` for users, comments, and terms as well.
544
+
545
+ **⚠️ This breaks backwards compatibility** for users using WP Query vars. Simply change your `x=y` parameter to `filter[x]=y`.
546
+
547
+ (props @WP-API, [#1420][gh-1420])
548
+
549
+ * Respect `rest_base` for taxonomies.
550
+
551
+ **⚠️ This breaks backwards compatibility** by changing the `/wp/v2/posts/{id}/terms/post_tag` endpoint to `/wp/v2/posts/{id}/tag`.
552
+
553
+ (props @joehoyle, [#1466][gh-1466])
554
+
555
+ * Add permission check for retrieving the posts collection in edit context.
556
+
557
+ By extension of the fact that getting any individual post yields a forbidden context error when the `context=edit` and the user is not authorized, the user should also not be permitted to list any post items when unauthorized.
558
+
559
+ (props @danielpunkass, [#1412][gh-1412])
560
+
561
+ * Ensure the REST API URL always has a trailing slash.
562
+
563
+ Previously, when pretty permalinks were enabled, the API URL during autodiscovery looked like `/wp-json`, whereas the non-pretty permalink URL looked like `?rest_route=/`. These are now consistent, and always end with a slash character to simplify client URL building.
564
+
565
+ (props @danielpunkass, @rmccue, [#1426][gh-1426], [#1442][gh-1442], [#1455][gh-1455], [#1467][gh-1467])
566
+
567
+ * Use `wp_json_encode` instead of `json_encode`
568
+
569
+ Since WordPress 4.1, `wp_json_encode` has been available to ensure encoded values are sane, and that non-UTF8 encodings are supported. We now use this function rather than doing the encode ourselves.
570
+
571
+ (props @rmccue, @pento, [#1417][gh-1417])
572
+
573
+ * Add `role` to schema for users.
574
+
575
+ The available roles you can assign to a user are now available in the schema as an `enum`.
576
+
577
+ (props @joehoyle, [#1400][gh-1400])
578
+
579
+ * Use the schema for validation inside the comments controller.
580
+
581
+ Previously, the schema was merely a decorative element for documentation inside the comments controller. To bring it inline with our other controllers, the schema is now used internally for validation.
582
+
583
+ (props @joehoyle, [#1422][gh-1422])
584
+
585
+ * Don't set the Location header in update responses.
586
+
587
+ Previously, the Location header was sent when updating resources due to some inadvertent copypasta. This header should only be sent when creating to direct clients to the new resource, and isn't required when you're already on the correct resource.
588
+
589
+ (props @rachelbaker, [#1441][gh-1441])
590
+
591
+ * Re-enable the `rest_insert_post` action hook for `WP_REST_Posts_Controller`
592
+
593
+ This was disabled during 2.0 development to avoid breaking lots of plugins on the `json_insert_post` action. Now that we've changed namespaces and are Mostly Stable (tm), we can re-enable the action.
594
+
595
+ (props @jaredcobb, [#1427][gh-1427], [#1424][gh-1424])
596
+
597
+ * Fix post taxonomy terms link URLs.
598
+
599
+ When moving the routes in a previous beta, we forgot to correct the links on post objects to the new correct route. Sorry!
600
+
601
+ (props @rachelbaker, @joehoyle, [#1447][gh-1447], [#1383][gh-1383])
602
+
603
+ * Use `wp_get_attachment_image_src()` on the image sizes in attachments.
604
+
605
+ Since the first versions of the API, we've been building attachment URLs via `str_replace`. Who knows why we were doing this, but it caused problems with custom attachment URLs (such as CDN-hosted images). This now correctly uses the internal functions and filters.
606
+
607
+ (props @joehoyle, [#1462][gh-1462])
608
+
609
+ * Make the embed context a default, not forced.
610
+
611
+ If you want embeds to bring in full data rather than with `context=edit`, you can now change the link to specify `context=view` explicitly.
612
+
613
+ (props @rmccue, [#1464][gh-1464])
614
+
615
+ * Ensure we always use the `term_taxonomy_id` and never expose `term_id` publicly.
616
+
617
+ Previously, `term_id` was inadvertently exposed in some error responses.
618
+
619
+ (props @jdolan, [#1430][gh-1430])
620
+
621
+ * Fix adding alt text to attachments on creation.
622
+
623
+ Previously, this could only be set when updating an attachment, not when creating one.
624
+
625
+ (props @joehoyle, [#1398][gh-1398])
626
+
627
+ * Throw an error when registering routes without a namespace.
628
+
629
+ Namespaces should **always** be provided when registering routes. We now throw a `doing_it_wrong` error when attempting to register one. (Previously, this caused a warning, or an invalid internal route.)
630
+
631
+ If you *really* need to register namespaceless routes (e.g. to replicate an existing API), call `WP_REST_Server::register_route` directly rather than using the convenience function.
632
+
633
+ (props @joehoyle, @rmccue, [#1355][gh-1355])
634
+
635
+ * Show links on embeds.
636
+
637
+ Previously, links were accidentally stripped from embedded response data.
638
+
639
+ (props @rmccue, [#1472][gh-1472])
640
+
641
+ * Clarify insufficient permisssion error when editing posts.
642
+
643
+ (props @danielpunkass, [#1411][gh-1411])
644
+
645
+ * Improve @return inline docs for rest_ensure_response()
646
+
647
+ (props @Shelob9, [#1328][gh-1328])
648
+
649
+ * Check taxonomies exist before trying to set properties.
650
+
651
+ (props @joehoyle, @rachelbaker, [#1354][gh-1354])
652
+
653
+ * Update controllers to ensure we use `sanitize_callback` wherever possible.
654
+
655
+ (props @joehoyle, [#1399][gh-1399])
656
+
657
+ * Add more phpDoc documentation, and correct existing documentation.
658
+
659
+ (props @Shelob9, @rmccue, [#1432][gh-1432], [#1433][gh-1433], [#1465][gh-1465])
660
+
661
+ * Update testing infrastructure.
662
+
663
+ Travis now runs our coding standards tests in parallel, and now uses the new, faster container-based testing infrastructure.
664
+
665
+ (props @ntwb, @frozzare, [#1449][gh-1449], [#1457][gh-1457])
666
+
667
+ [View all changes](https://github.com/WP-API/WP-API/compare/2.0-beta3...2.0-beta4)
668
+
669
+ [gh-839]: https://github.com/WP-API/WP-API/issues/839
670
+ [gh-865]: https://github.com/WP-API/WP-API/issues/865
671
+ [gh-1222]: https://github.com/WP-API/WP-API/issues/1222
672
+ [gh-1305]: https://github.com/WP-API/WP-API/issues/1305
673
+ [gh-1310]: https://github.com/WP-API/WP-API/issues/1310
674
+ [gh-1320]: https://github.com/WP-API/WP-API/issues/1320
675
+ [gh-1328]: https://github.com/WP-API/WP-API/issues/1328
676
+ [gh-1354]: https://github.com/WP-API/WP-API/issues/1354
677
+ [gh-1355]: https://github.com/WP-API/WP-API/issues/1355
678
+ [gh-1372]: https://github.com/WP-API/WP-API/issues/1372
679
+ [gh-1374]: https://github.com/WP-API/WP-API/issues/1374
680
+ [gh-1383]: https://github.com/WP-API/WP-API/issues/1383
681
+ [gh-1397]: https://github.com/WP-API/WP-API/issues/1397
682
+ [gh-1398]: https://github.com/WP-API/WP-API/issues/1398
683
+ [gh-1399]: https://github.com/WP-API/WP-API/issues/1399
684
+ [gh-1400]: https://github.com/WP-API/WP-API/issues/1400
685
+ [gh-1402]: https://github.com/WP-API/WP-API/issues/1402
686
+ [gh-1411]: https://github.com/WP-API/WP-API/issues/1411
687
+ [gh-1412]: https://github.com/WP-API/WP-API/issues/1412
688
+ [gh-1413]: https://github.com/WP-API/WP-API/issues/1413
689
+ [gh-1415]: https://github.com/WP-API/WP-API/issues/1415
690
+ [gh-1417]: https://github.com/WP-API/WP-API/issues/1417
691
+ [gh-1420]: https://github.com/WP-API/WP-API/issues/1420
692
+ [gh-1422]: https://github.com/WP-API/WP-API/issues/1422
693
+ [gh-1424]: https://github.com/WP-API/WP-API/issues/1424
694
+ [gh-1426]: https://github.com/WP-API/WP-API/issues/1426
695
+ [gh-1427]: https://github.com/WP-API/WP-API/issues/1427
696
+ [gh-1430]: https://github.com/WP-API/WP-API/issues/1430
697
+ [gh-1432]: https://github.com/WP-API/WP-API/issues/1432
698
+ [gh-1433]: https://github.com/WP-API/WP-API/issues/1433
699
+ [gh-1435]: https://github.com/WP-API/WP-API/issues/1435
700
+ [gh-1441]: https://github.com/WP-API/WP-API/issues/1441
701
+ [gh-1442]: https://github.com/WP-API/WP-API/issues/1442
702
+ [gh-1447]: https://github.com/WP-API/WP-API/issues/1447
703
+ [gh-1449]: https://github.com/WP-API/WP-API/issues/1449
704
+ [gh-1455]: https://github.com/WP-API/WP-API/issues/1455
705
+ [gh-1455]: https://github.com/WP-API/WP-API/issues/1455
706
+ [gh-1457]: https://github.com/WP-API/WP-API/issues/1457
707
+ [gh-1459]: https://github.com/WP-API/WP-API/issues/1459
708
+ [gh-1462]: https://github.com/WP-API/WP-API/issues/1462
709
+ [gh-1464]: https://github.com/WP-API/WP-API/issues/1464
710
+ [gh-1465]: https://github.com/WP-API/WP-API/issues/1465
711
+ [gh-1466]: https://github.com/WP-API/WP-API/issues/1466
712
+ [gh-1467]: https://github.com/WP-API/WP-API/issues/1467
713
+ [gh-1472]: https://github.com/WP-API/WP-API/issues/1472
714
+
715
+ = 2.0 Beta 3.0 =
716
+
717
+ * Add ability to declare sanitization and default options for schema fields.
718
+
719
+ The `arg_options` array can be used to declare the sanitization callback,
720
+ default value, or requirement of a field.
721
+
722
+ (props @joehoyle, [#1345][gh-1345])
723
+ (props @joehoyle, [#1346][gh-1346])
724
+
725
+ * Expand supported parameters for creating and updating Comments.
726
+
727
+ (props @rachelbaker, [#1245][gh-1245])
728
+
729
+ * Declare collection parameters for Terms of a Post.
730
+
731
+ Define the available collection parameters in `get_collection_params()` and
732
+ allow Terms of a Post to be queried by term order.
733
+
734
+ (props @danielbachhuber, [#1332][gh-1332])
735
+
736
+ * Improve the Attachment error message for an invalid Content-Disposition
737
+
738
+ (props @danielbachhuber, [#1317][gh-1317])
739
+
740
+ * Return 200 status when updating Attachments, Comments, and Users.
741
+
742
+ (props @rachelbaker, [#1348][gh-1348])
743
+
744
+ * Remove unnecessary `handle_format_param()` method.
745
+
746
+ (props @danielbachhuber, [#1331][gh-1331])
747
+
748
+ * Add `author_avatar_url` field to the Comment response and schema.
749
+
750
+ (props @rachelbaker [#1327][gh-1327])
751
+
752
+ * Introduce `rest_do_request()` for making REST requests internally.
753
+
754
+ (props @danielbachhuber, [#1333][gh-1333])
755
+
756
+ * Remove unused DateTime class.
757
+
758
+ (props @rmccue, [#1314][gh-1314])
759
+
760
+ * Add inline documentation for `$wp_rest_server` global.
761
+
762
+ (props @Shelob9, [#1324][gh-1324])
763
+
764
+ [View all changes](https://github.com/WP-API/WP-API/compare/2.0-beta2...2.0-beta3)
765
+ [gh-1245]: https://github.com/WP-API/WP-API/issues/1245
766
+ [gh-1314]: https://github.com/WP-API/WP-API/issues/1314
767
+ [gh-1317]: https://github.com/WP-API/WP-API/issues/1317
768
+ [gh-1318]: https://github.com/WP-API/WP-API/issues/1318
769
+ [gh-1324]: https://github.com/WP-API/WP-API/issues/1324
770
+ [gh-1326]: https://github.com/WP-API/WP-API/issues/1326
771
+ [gh-1327]: https://github.com/WP-API/WP-API/issues/1327
772
+ [gh-1331]: https://github.com/WP-API/WP-API/issues/1331
773
+ [gh-1332]: https://github.com/WP-API/WP-API/issues/1332
774
+ [gh-1333]: https://github.com/WP-API/WP-API/issues/1333
775
+ [gh-1345]: https://github.com/WP-API/WP-API/issues/1345
776
+ [gh-1346]: https://github.com/WP-API/WP-API/issues/1346
777
+ [gh-1347]: https://github.com/WP-API/WP-API/issues/1347
778
+ [gh-1348]: https://github.com/WP-API/WP-API/issues/1348
779
+
780
+ = 2.0 Beta 2.0 =
781
+
782
+ * Load the WP REST API before the main query runs.
783
+
784
+ The `rest_api_loaded` function now hooks into the `parse_request` action.
785
+ This change prevents the main query from being run on every request and
786
+ allows sites to set `WP_USE_THEMES` to `false`. Previously, the main query
787
+ was always being run (`SELECT * FROM wp_posts LIMIT 10`), even though the
788
+ result was never used and couldn't be cached.
789
+
790
+ (props @rmccue, [#1270][gh-1270])
791
+
792
+ * Register a new field on an existing WordPress object type.
793
+
794
+ Introduces `register_api_field()` to add a field to an object and
795
+ its schema.
796
+
797
+ (props @joehoyle, @rachelbaker, [#927][gh-927])
798
+ (props @joehoyle, [#1207][gh-1207])
799
+ (props @joehoyle, [#1243][gh-1243])
800
+
801
+ * Add endpoints for viewing, creating, updating, and deleting Terms for a Post.
802
+
803
+ The new `WP_REST_Posts_Terms_Controller` class controller supports routes for
804
+ Terms that belong to a Post.
805
+
806
+ (props @joehoyle, @danielbachhuber, [#1216][gh-1216])
807
+
808
+ * Add pagination headers for collection queries.
809
+
810
+ The `X-WP-Total` and `X-WP-TotalPages` are now present in terms, comments,
811
+ and users collection responses.
812
+
813
+ (props @danielbachhuber, [#1182][gh-1182])
814
+ (props @danielbachhuber, [#1191][gh-1191])
815
+ (props @danielbachhuber, @joehoyle, [#1197][gh-1197])
816
+
817
+ * List registered namespaces in the index for feature detection.
818
+
819
+ The index (`/wp-json` by default) now contains a list of the available
820
+ namespaces. This allows for simple feature detection. You can grab the index
821
+ and check namespaces for `wp/v3` or `pluginname/v2`, which indicate the
822
+ supported endpoints on the site.
823
+
824
+ (props @rmccue,, [#1283][gh-1283])
825
+
826
+ * Standardize link property relations and support embedding for all resources.
827
+
828
+ Change link properties to use IANA-registered relations. Also adds embedding
829
+ support to Attachments, Comments and Terms.
830
+
831
+ (props @rmccue, @rachelbaker, [#1284][gh-1284])
832
+
833
+ * Add support for Composer dependency management.
834
+
835
+ Allows you to recursively install/update the WP REST API inside of WordPress
836
+ plugins or themes.
837
+
838
+ (props @QWp6t, [#1157][gh-1157])
839
+
840
+ * Return full objects in the delete response.
841
+
842
+ Instead of returning a random message when deleting a Post, Comment, Term, or
843
+ User provide the original resource data.
844
+
845
+ (props @danielbachhuber, [#1253][gh-1253])
846
+ (props @danielbachhuber, [#1254][gh-1254])
847
+ (props @danielbachhuber, [#1255][gh-1255])
848
+ (props @danielbachhuber, [#1256][gh-1256])
849
+
850
+ * Return programmatically readable error messages for invalid or missing
851
+ required parameters.
852
+
853
+ (props @joehoyle, [#1175][gh-1175])
854
+
855
+ * Declare supported arguments for Comment and User collection queries.
856
+
857
+ (props @danielbachhuber, [#1211][gh-1211])
858
+ (props @danielbachhuber, [#1217][gh-1217])
859
+
860
+ * Automatically validate parameters based on Schema data.
861
+
862
+ (props @joehoyle, [#1128][gh-1128])
863
+
864
+ * Use the `show_in_rest` attributes for exposing Taxonomies.
865
+
866
+ (props @joehoyle, [#1279][gh-1279])
867
+
868
+ * Handle `parent` when creating or updating a Term.
869
+
870
+ (props @joehoyle, [#1221][gh-1221])
871
+
872
+ * Limit fields returned in `embed` context User responses.
873
+
874
+ (props @rachelbaker, [#1251][gh-1251])
875
+
876
+ * Only include `parent` in term response when tax is hierarchical.
877
+
878
+ (props @danielbachhuber, [#1189][gh-1189])
879
+
880
+ * Fix bug in creating comments if `type` was not set.
881
+
882
+ (props @rachelbaker, [#1244][gh-1244])
883
+
884
+ * Rename `post_name` field to `post_slug`.
885
+
886
+ (props @danielbachhuber, [#1235][gh-1235])
887
+
888
+ * Add check when creating a user to verify the provided role is valid.
889
+
890
+ (props @rachelbaker, [#1267][gh-1267])
891
+
892
+ * Add link properties to the Post Status response.
893
+
894
+ (props @joehoyle, [#1243][gh-1243])
895
+
896
+ * Return `0` for `parent` in Post response instead of `null`.
897
+
898
+ (props @danielbachhuber, [#1269][gh-1269])
899
+
900
+ * Only link `author` when there's a valid author
901
+
902
+ (props @danielbachhuber, [#1203][gh-1203])
903
+
904
+ * Only permit querying by parent term when tax is hierarchical.
905
+
906
+ (props @danielbachhuber, [#1219][gh-1219])
907
+
908
+ * Only permit deleting posts of the proper type
909
+
910
+ (props @danielbachhuber, [#1257][gh-1257])
911
+
912
+ * Set pagination headers even when no found posts.
913
+
914
+ (props @danielbachhuber, [#1209][gh-1209])
915
+
916
+ * Correct prefix in `rest_request_parameter_order` filter.
917
+
918
+ (props @quasel, [#1158][gh-1158])
919
+
920
+ * Retool `WP_REST_Terms_Controller` to follow Posts controller pattern.
921
+
922
+ (props @danielbachhuber, [#1170][gh-1170])
923
+
924
+ * Remove unused `accept_json argument` from the `register_routes` method.
925
+
926
+ (props @quasel, [#1160][gh-1160])
927
+
928
+ * Fix typo in `sanitize_params` inline documentation.
929
+
930
+ (props @Shelob9, [#1226][gh-1226])
931
+
932
+ * Remove commented out code in dispatch method.
933
+
934
+ (props @rachelbaker, [#1162][gh-1162])
935
+
936
+
937
+ [View all changes](https://github.com/WP-API/WP-API/compare/2.0-beta1.1...2.0-beta2)
938
+ [gh-927]: https://github.com/WP-API/WP-API/issues/927
939
+ [gh-1128]: https://github.com/WP-API/WP-API/issues/1128
940
+ [gh-1157]: https://github.com/WP-API/WP-API/issues/1157
941
+ [gh-1158]: https://github.com/WP-API/WP-API/issues/1158
942
+ [gh-1160]: https://github.com/WP-API/WP-API/issues/1160
943
+ [gh-1162]: https://github.com/WP-API/WP-API/issues/1162
944
+ [gh-1168]: https://github.com/WP-API/WP-API/issues/1168
945
+ [gh-1170]: https://github.com/WP-API/WP-API/issues/1170
946
+ [gh-1171]: https://github.com/WP-API/WP-API/issues/1171
947
+ [gh-1175]: https://github.com/WP-API/WP-API/issues/1175
948
+ [gh-1176]: https://github.com/WP-API/WP-API/issues/1176
949
+ [gh-1177]: https://github.com/WP-API/WP-API/issues/1177
950
+ [gh-1181]: https://github.com/WP-API/WP-API/issues/1181
951
+ [gh-1182]: https://github.com/WP-API/WP-API/issues/1182
952
+ [gh-1188]: https://github.com/WP-API/WP-API/issues/1188
953
+ [gh-1189]: https://github.com/WP-API/WP-API/issues/1189
954
+ [gh-1191]: https://github.com/WP-API/WP-API/issues/1191
955
+ [gh-1197]: https://github.com/WP-API/WP-API/issues/1197
956
+ [gh-1200]: https://github.com/WP-API/WP-API/issues/1200
957
+ [gh-1203]: https://github.com/WP-API/WP-API/issues/1203
958
+ [gh-1207]: https://github.com/WP-API/WP-API/issues/1207
959
+ [gh-1209]: https://github.com/WP-API/WP-API/issues/1209
960
+ [gh-1210]: https://github.com/WP-API/WP-API/issues/1210
961
+ [gh-1211]: https://github.com/WP-API/WP-API/issues/1211
962
+ [gh-1216]: https://github.com/WP-API/WP-API/issues/1216
963
+ [gh-1217]: https://github.com/WP-API/WP-API/issues/1217
964
+ [gh-1219]: https://github.com/WP-API/WP-API/issues/1219
965
+ [gh-1221]: https://github.com/WP-API/WP-API/issues/1221
966
+ [gh-1226]: https://github.com/WP-API/WP-API/issues/1226
967
+ [gh-1235]: https://github.com/WP-API/WP-API/issues/1235
968
+ [gh-1243]: https://github.com/WP-API/WP-API/issues/1243
969
+ [gh-1244]: https://github.com/WP-API/WP-API/issues/1244
970
+ [gh-1249]: https://github.com/WP-API/WP-API/issues/1249
971
+ [gh-1251]: https://github.com/WP-API/WP-API/issues/1251
972
+ [gh-1253]: https://github.com/WP-API/WP-API/issues/1253
973
+ [gh-1254]: https://github.com/WP-API/WP-API/issues/1254
974
+ [gh-1255]: https://github.com/WP-API/WP-API/issues/1255
975
+ [gh-1256]: https://github.com/WP-API/WP-API/issues/1256
976
+ [gh-1257]: https://github.com/WP-API/WP-API/issues/1257
977
+ [gh-1259]: https://github.com/WP-API/WP-API/issues/1259
978
+ [gh-1267]: https://github.com/WP-API/WP-API/issues/1267
979
+ [gh-1268]: https://github.com/WP-API/WP-API/issues/1268
980
+ [gh-1269]: https://github.com/WP-API/WP-API/issues/1269
981
+ [gh-1270]: https://github.com/WP-API/WP-API/issues/1270
982
+ [gh-1276]: https://github.com/WP-API/WP-API/issues/1276
983
+ [gh-1277]: https://github.com/WP-API/WP-API/issues/1277
984
+ [gh-1279]: https://github.com/WP-API/WP-API/issues/1279
985
+ [gh-1283]: https://github.com/WP-API/WP-API/issues/1283
986
+ [gh-1284]: https://github.com/WP-API/WP-API/issues/1284
987
+ [gh-1295]: https://github.com/WP-API/WP-API/issues/1295
988
+ [gh-1301]: https://github.com/WP-API/WP-API/issues/1301
989
+
990
+
991
+ = 2.0 Beta 1.1 =
992
+
993
+ * Fix user access security vulnerability.
994
+
995
+ Authenticated users were able to escalate their privileges bypassing the
996
+ expected capabilities check.
997
+
998
+ Reported by @kacperszurek on 2015-05-16.
999
+
1000
+ = 2.0 Beta 1 =
1001
 
1002
  Partial rewrite and evolution of the REST API to prepare for core integration.
1003
 
wp-api.js CHANGED
@@ -13,18 +13,25 @@
13
 
14
  })( window );
15
 
16
- (function( Backbone, _, window, undefined ) {
 
 
17
 
18
- //'use strict';
 
 
19
 
20
- // ECMAScript 5 shim, from MDN
21
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
 
 
22
  if ( ! Date.prototype.toISOString ) {
23
  var pad = function( number ) {
24
  var r = String( number );
25
  if ( r.length === 1 ) {
26
  r = '0' + r;
27
  }
 
28
  return r;
29
  };
30
 
@@ -35,88 +42,83 @@
35
  'T' + pad( this.getUTCHours() ) +
36
  ':' + pad( this.getUTCMinutes() ) +
37
  ':' + pad( this.getUTCSeconds() ) +
38
- '.' + String( ( this.getUTCMilliseconds()/1000 ).toFixed( 3 ) ).slice( 2, 5 ) +
39
  'Z';
40
  };
41
  }
42
 
43
- function WP_API_Utils() {
44
- var origParse = Date.parse,
 
 
 
 
 
 
45
  numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ];
46
 
 
 
 
 
 
 
 
 
 
47
 
48
- this.parseISO8601 = function( date ) {
49
- var timestamp, struct, i, k,
50
- minutesOffset = 0;
51
-
52
- // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string
53
- // before falling back to any implementation-specific date parsing, so that’s what we do, even if native
54
- // implementations could be faster
55
- // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm
56
- if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) {
57
- // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC
58
- for ( i = 0; ( k = numericKeys[i] ); ++i) {
59
- struct[k] = +struct[k] || 0;
60
- }
61
-
62
- // allow undefined days and months
63
- struct[2] = ( +struct[2] || 1 ) - 1;
64
- struct[3] = +struct[3] || 1;
65
 
66
- if ( struct[8] !== 'Z' && struct[9] !== undefined ) {
67
- minutesOffset = struct[10] * 60 + struct[11];
68
 
69
- if ( struct[9] === '+' ) {
70
- minutesOffset = 0 - minutesOffset;
71
- }
72
  }
73
-
74
- timestamp = Date.UTC( struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7] );
75
- }
76
- else {
77
- timestamp = origParse ? origParse( date ) : NaN;
78
  }
79
 
80
- return timestamp;
81
- };
82
- }
 
83
 
84
- window.wp = window.wp || {};
85
- wp.api = wp.api || {};
86
- wp.api.utils = wp.api.utils || new WP_API_Utils();
87
 
88
- })( Backbone, _, window );
89
 
90
  /* global WP_API_Settings:false */
91
  // Suppress warning about parse function's unused "options" argument:
92
  /* jshint unused:false */
93
- (function( wp, WP_API_Settings, Backbone, _, window, undefined ) {
94
 
95
  'use strict';
96
 
97
  /**
98
- * Array of parseable dates
99
  *
100
- * @type {string[]}
101
  */
102
  var parseable_dates = [ 'date', 'modified', 'date_gmt', 'modified_gmt' ];
103
 
104
  /**
105
- * Mixin for all content that is time stamped
106
  *
107
- * @type {{toJSON: toJSON, parse: parse}}
108
  */
109
  var TimeStampedMixin = {
110
  /**
111
- * Serialize the entity pre-sync
112
  *
113
- * @returns {*}
114
  */
115
  toJSON: function() {
116
  var attributes = _.clone( this.attributes );
117
 
118
- // Serialize Date objects back into 8601 strings
119
- _.each( parseable_dates, function ( key ) {
120
  if ( key in attributes ) {
121
  attributes[key] = attributes[key].toISOString();
122
  }
@@ -126,13 +128,14 @@
126
  },
127
 
128
  /**
129
- * Unserialize the fetched response
130
  *
131
- * @param {*} response
132
- * @returns {*}
133
  */
134
  parse: function( response ) {
135
- // Parse dates into native Date objects
 
136
  _.each( parseable_dates, function ( key ) {
137
  if ( ! ( key in response ) ) {
138
  return;
@@ -142,8 +145,8 @@
142
  response[key] = new Date( timestamp );
143
  });
144
 
145
- // Parse the author into a User object
146
- if ( response.author !== 'undefined' ) {
147
  response.author = new wp.api.models.User( response.author );
148
  }
149
 
@@ -152,13 +155,13 @@
152
  };
153
 
154
  /**
155
- * Mixin for all hierarchical content types such as posts
156
  *
157
- * @type {{parent: parent}}
158
  */
159
  var HierarchicalMixin = {
160
  /**
161
- * Get parent object
162
  *
163
  * @returns {Backbone.Model}
164
  */
@@ -166,14 +169,14 @@
166
 
167
  var object, parent = this.get( 'parent' );
168
 
169
- // Return null if we don't have a parent
170
  if ( parent === 0 ) {
171
  return null;
172
  }
173
 
174
  var parentModel = this;
175
 
176
- if ( typeof this.parentModel !== 'undefined' ) {
177
  /**
178
  * Probably a better way to do this. Perhaps grab a cached version of the
179
  * instantiated model?
@@ -185,36 +188,38 @@
185
  if ( parentModel.collection ) {
186
  return parentModel.collection.get( parent );
187
  } else {
188
- // Otherwise, get the object directly
 
189
  object = new parentModel.constructor( {
190
- ID: parent
191
  });
192
 
193
- // Note that this acts asynchronously
194
  object.fetch();
 
195
  return object;
196
  }
197
  }
198
  };
199
 
200
  /**
201
- * Private Backbone base model for all models
202
  */
203
- var BaseModel = Backbone.Model.extend(
204
- /** @lends BaseModel.prototype */
205
  {
206
  /**
207
- * Set nonce header before every Backbone sync
208
  *
209
- * @param {string} method
210
- * @param {Backbone.Model} model
211
- * @param {{beforeSend}, *} options
212
- * @returns {*}
213
  */
214
  sync: function( method, model, options ) {
215
  options = options || {};
216
 
217
- if ( typeof WP_API_Settings.nonce !== 'undefined' ) {
218
  var beforeSend = options.beforeSend;
219
 
220
  options.beforeSend = function( xhr ) {
@@ -232,387 +237,344 @@
232
  );
233
 
234
  /**
235
- * Backbone model for single users
 
 
 
 
236
  */
237
- wp.api.models.User = BaseModel.extend(
238
  /** @lends User.prototype */
239
  {
240
- idAttribute: 'ID',
241
 
242
- urlRoot: WP_API_Settings.root + '/users',
243
 
244
  defaults: {
245
- ID: null,
246
- username: '',
 
 
247
  email: '',
248
- password: '',
249
- name: '',
250
  first_name: '',
251
  last_name: '',
 
 
252
  nickname: '',
 
 
253
  slug: '',
254
- URL: '',
255
- avatar: '',
256
- meta: {
257
- links: {}
258
- }
259
- },
260
-
261
- /**
262
- * Return avatar URL
263
- *
264
- * @param {number} size
265
- * @returns {string}
266
- */
267
- avatar: function( size ) {
268
- return this.get( 'avatar' ) + '&s=' + size;
269
  }
270
  }
271
  );
272
 
273
  /**
274
- * Model for Taxonomy
 
 
 
275
  */
276
- wp.api.models.Taxonomy = BaseModel.extend(
277
  /** @lends Taxonomy.prototype */
278
  {
279
  idAttribute: 'slug',
280
 
281
- urlRoot: WP_API_Settings.root + '/taxonomies',
282
 
283
  defaults: {
284
  name: '',
285
  slug: null,
 
286
  labels: {},
287
- types: {},
288
  show_cloud: false,
289
- hierarchical: false,
290
- meta: {
291
- links: {}
292
- }
293
  }
294
  }
295
  );
296
 
297
  /**
298
- * Backbone model for term
 
 
 
299
  */
300
- wp.api.models.Term = BaseModel.extend( _.extend(
301
  /** @lends Term.prototype */
302
  {
303
- idAttribute: 'ID',
304
-
305
- taxonomy: 'category',
306
-
307
- /**
308
- * @class Represent a term
309
- * @augments Backbone.Model
310
- * @constructs
311
- */
312
- initialize: function( attributes, options ) {
313
- if ( typeof options !== 'undefined' ) {
314
- if ( options.taxonomy ) {
315
- this.taxonomy = options.taxonomy;
316
- }
317
- }
318
- },
319
-
320
- /**
321
- * Return URL for the model
322
- *
323
- * @returns {string}
324
- */
325
- url: function() {
326
- var id = this.get( 'ID' );
327
- id = id || '';
328
 
329
- return WP_API_Settings.root + '/taxonomies/' + this.taxonomy + '/terms/' + id;
330
- },
331
 
332
  defaults: {
333
- ID: null,
334
  name: '',
335
  slug: '',
336
  description: '',
337
  parent: null,
338
  count: 0,
339
  link: '',
340
- meta: {
341
- links: {}
342
- }
343
  }
344
 
345
- }, TimeStampedMixin, HierarchicalMixin )
346
  );
347
 
348
  /**
349
- * Backbone model for single posts
 
 
 
350
  */
351
- wp.api.models.Post = BaseModel.extend( _.extend(
352
  /** @lends Post.prototype */
353
  {
354
- idAttribute: 'ID',
355
 
356
- urlRoot: WP_API_Settings.root + '/posts',
357
 
358
  defaults: {
359
- ID: null,
360
- title: '',
361
- status: 'draft',
362
- type: 'post',
363
- author: new wp.api.models.User(),
364
- content: '',
365
- link: '',
366
- 'parent': 0,
367
  date: new Date(),
368
  date_gmt: new Date(),
 
 
369
  modified: new Date(),
370
  modified_gmt: new Date(),
371
- format: 'standard',
372
- slug: '',
373
- guid: '',
374
- excerpt: '',
375
- menu_order: 0,
 
 
 
376
  comment_status: 'open',
377
  ping_status: 'open',
378
  sticky: false,
379
- date_tz: 'Etc/UTC',
380
- modified_tz: 'Etc/UTC',
381
- featured_image: null,
382
- terms: {},
383
- post_meta: {},
384
- meta: {
385
- links: {}
386
- }
387
  }
388
  }, TimeStampedMixin, HierarchicalMixin )
389
  );
390
 
391
  /**
392
- * Backbone model for pages
 
 
 
393
  */
394
- wp.api.models.Page = BaseModel.extend( _.extend(
395
  /** @lends Page.prototype */
396
  {
397
- idAttribute: 'ID',
398
 
399
- urlRoot: WP_API_Settings.root + '/pages',
400
 
401
  defaults: {
402
- ID: null,
403
- title: '',
404
- status: 'draft',
405
- type: 'page',
406
- author: new wp.api.models.User(),
407
- content: '',
408
- parent: 0,
409
- link: '',
410
  date: new Date(),
411
- modified: new Date(),
412
  date_gmt: new Date(),
 
 
 
413
  modified_gmt: new Date(),
414
- date_tz: 'Etc/UTC',
415
- modified_tz: 'Etc/UTC',
416
- format: 'standard',
417
- slug: '',
418
- guid: '',
419
- excerpt: '',
420
- menu_order: 0,
421
- comment_status: 'closed',
422
- ping_status: 'open',
423
- sticky: false,
424
  password: '',
425
- meta: {
426
- links: {}
427
- },
 
 
 
 
428
  featured_image: null,
429
- terms: []
 
 
 
 
430
  }
431
  }, TimeStampedMixin, HierarchicalMixin )
432
  );
433
 
434
  /**
435
- * Backbone model for revisions
 
 
 
 
436
  */
437
- wp.api.models.Revision = wp.api.models.Post.extend(
438
- /** @lends Revision.prototype */
439
  {
440
- /**
441
- * Return URL for model
442
- *
443
- * @returns {string}
444
- */
445
- url: function() {
446
- var parent_id = this.get( 'parent' );
447
- parent_id = parent_id || '';
448
-
449
- var id = this.get( 'ID' );
450
- id = id || '';
451
 
452
- return WP_API_Settings.root + '/posts/' + parent_id + '/revisions/' + id;
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  },
454
 
455
  /**
456
- * @class Represent a revision
457
- * @augments Backbone.Model
458
- * @constructs
459
  */
460
- initialize: function() {
461
- // Todo: what of the parent model is a page?
462
- this.parentModel = wp.api.models.Post;
 
 
463
  }
464
- }
 
465
  );
466
 
467
  /**
468
- * Backbone model for media items
 
 
 
469
  */
470
- wp.api.models.Media = BaseModel.extend( _.extend(
471
  /** @lends Media.prototype */
472
  {
473
- idAttribute: 'ID',
474
 
475
- urlRoot: WP_API_Settings.root + '/media',
476
 
477
  defaults: {
478
- ID: null,
479
- title: '',
480
- status: 'inherit',
481
- type: 'attachment',
482
- author: new wp.api.models.User(),
483
- content: '',
484
- parent: 0,
485
- link: '',
486
  date: new Date(),
 
 
 
487
  modified: new Date(),
488
- format: 'standard',
 
489
  slug: '',
490
- guid: '',
491
- excerpt: '',
492
- menu_order: 0,
 
493
  comment_status: 'open',
494
  ping_status: 'open',
495
- sticky: false,
496
- date_tz: 'Etc/UTC',
497
- modified_tz: 'Etc/UTC',
498
- date_gmt: new Date(),
499
- modified_gmt: new Date(),
500
- meta: {
501
- links: {}
502
- },
503
- terms: [],
504
- source: '',
505
- is_image: true,
506
- attachment_meta: {},
507
- image_meta: {}
508
- },
509
-
510
- /**
511
- * @class Represent a media item
512
- * @augments Backbone.Model
513
- * @constructs
514
- */
515
- initialize: function() {
516
- // Todo: what of the parent model is a page?
517
- this.parentModel = wp.api.models.Post;
518
  }
519
- }, TimeStampedMixin, HierarchicalMixin )
 
520
  );
521
 
522
  /**
523
- * Backbone model for comments
 
 
 
524
  */
525
- wp.api.models.Comment = BaseModel.extend( _.extend(
526
  /** @lends Comment.prototype */
527
  {
528
- idAttribute: 'ID',
 
 
529
 
530
  defaults: {
531
- ID: null,
532
- post: null,
533
- content: '',
534
- status: 'hold',
535
- type: '',
536
- parent: 0,
537
- author: new wp.api.models.User(),
 
538
  date: new Date(),
539
  date_gmt: new Date(),
540
- date_tz: 'Etc/UTC',
541
- meta: {
542
- links: {}
543
- }
544
- },
545
-
546
- /**
547
- * Return URL for model
548
- *
549
- * @returns {string}
550
- */
551
- url: function() {
552
- var post_id = this.get( 'post' );
553
- post_id = post_id || '';
554
-
555
- var id = this.get( 'ID' );
556
- id = id || '';
557
-
558
- return WP_API_Settings.root + '/posts/' + post_id + '/comments/' + id;
559
  }
 
560
  }, TimeStampedMixin, HierarchicalMixin )
561
  );
562
 
563
  /**
564
- * Backbone model for single post types
 
 
 
565
  */
566
- wp.api.models.PostType = BaseModel.extend(
567
  /** @lends PostType.prototype */
568
  {
569
  idAttribute: 'slug',
570
 
571
- urlRoot: WP_API_Settings.root + '/posts/types',
572
 
573
  defaults: {
574
  slug: null,
575
  name: '',
576
  description: '',
577
  labels: {},
578
- queryable: false,
579
- searchable: false,
580
- hierarchical: false,
581
- meta: {
582
- links: {}
583
- },
584
- taxonomies: []
585
  },
586
 
587
  /**
588
- * Prevent model from being saved
589
  *
590
- * @returns {boolean}
591
  */
592
- save: function () {
593
  return false;
594
  },
595
 
596
  /**
597
- * Prevent model from being deleted
598
  *
599
- * @returns {boolean}
600
  */
601
- 'delete': function () {
602
  return false;
603
  }
604
  }
605
  );
606
 
607
  /**
608
- * Backbone model for a post status
 
 
 
609
  */
610
- wp.api.models.PostStatus = BaseModel.extend(
611
  /** @lends PostStatus.prototype */
612
  {
613
  idAttribute: 'slug',
614
 
615
- urlRoot: WP_API_Settings.root + '/posts/statuses',
616
 
617
  defaults: {
618
  slug: null,
@@ -622,44 +584,80 @@
622
  'private': false,
623
  queryable: true,
624
  show_in_list: true,
625
- meta: {
626
- links: {}
627
- }
628
  },
629
 
630
  /**
631
- * Prevent model from being saved
632
  *
633
- * @returns {boolean}
634
  */
635
  save: function() {
636
  return false;
637
  },
638
 
639
  /**
640
- * Prevent model from being deleted
641
  *
642
- * @returns {boolean}
643
  */
644
- 'delete': function() {
645
  return false;
646
  }
647
  }
648
  );
649
 
650
- })( wp, WP_API_Settings, Backbone, _, window );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
 
652
  /* global WP_API_Settings:false */
653
  (function( wp, WP_API_Settings, Backbone, _, window, undefined ) {
654
 
655
  'use strict';
656
 
 
 
 
657
  var BaseCollection = Backbone.Collection.extend(
658
  /** @lends BaseCollection.prototype */
659
  {
660
 
661
  /**
662
- * Setup default state
663
  */
664
  initialize: function() {
665
  this.state = {
@@ -675,53 +673,52 @@
675
  *
676
  * Set nonce header before every Backbone sync.
677
  *
678
- * @param {string} method
679
- * @param {Backbone.Model} model
680
- * @param {{success}, *} options
681
- * @returns {*}
682
  */
683
  sync: function( method, model, options ) {
684
  options = options || {};
685
- var beforeSend = options.beforeSend;
 
686
 
687
- if ( typeof WP_API_Settings.nonce !== 'undefined' ) {
688
  options.beforeSend = function( xhr ) {
689
  xhr.setRequestHeader( 'X-WP-Nonce', WP_API_Settings.nonce );
690
 
691
  if ( beforeSend ) {
692
- return beforeSend.apply( this, arguments );
693
  }
694
  };
695
  }
696
 
697
  if ( 'read' === method ) {
698
- var SELF = this;
699
-
700
  if ( options.data ) {
701
- SELF.state.data = _.clone( options.data );
702
 
703
- delete SELF.state.data.page;
704
  } else {
705
- SELF.state.data = options.data = {};
706
  }
707
 
708
- if ( typeof options.data.page === 'undefined' ) {
709
- SELF.state.currentPage = null;
710
- SELF.state.totalPages = null;
711
- SELF.state.totalObjects = null;
712
  } else {
713
- SELF.state.currentPage = options.data.page - 1;
714
  }
715
 
716
  var success = options.success;
717
  options.success = function( data, textStatus, request ) {
718
- SELF.state.totalPages = parseInt( request.getResponseHeader( 'X-WP-TotalPages' ), 10 );
719
- SELF.state.totalObjects = parseInt( request.getResponseHeader( 'X-WP-Total' ), 10 );
720
 
721
- if ( SELF.state.currentPage === null ) {
722
- SELF.state.currentPage = 1;
723
  } else {
724
- SELF.state.currentPage++;
725
  }
726
 
727
  if ( success ) {
@@ -734,10 +731,10 @@
734
  },
735
 
736
  /**
737
- * Fetches the next page of objects if a new page exists
738
  *
739
- * @param {data: {page}} options
740
- * @returns {*}
741
  */
742
  more: function( options ) {
743
  options = options || {};
@@ -745,7 +742,7 @@
745
 
746
  _.extend( options.data, this.state.data );
747
 
748
- if ( typeof options.data.page === 'undefined' ) {
749
  if ( ! this.hasMore() ) {
750
  return false;
751
  }
@@ -761,9 +758,9 @@
761
  },
762
 
763
  /**
764
- * Returns true if there are more pages of objects available
765
  *
766
- * @returns null|boolean
767
  */
768
  hasMore: function() {
769
  if ( this.state.totalPages === null ||
@@ -778,180 +775,170 @@
778
  );
779
 
780
  /**
781
- * Backbone collection for posts
782
  */
783
  wp.api.collections.Posts = BaseCollection.extend(
784
  /** @lends Posts.prototype */
785
  {
786
- url: WP_API_Settings.root + '/posts',
787
 
788
  model: wp.api.models.Post
789
  }
790
  );
791
 
792
  /**
793
- * Backbone collection for pages
794
  */
795
  wp.api.collections.Pages = BaseCollection.extend(
796
  /** @lends Pages.prototype */
797
  {
798
- url: WP_API_Settings.root + '/pages',
799
 
800
  model: wp.api.models.Page
801
  }
802
  );
803
 
804
  /**
805
- * Backbone users collection
806
  */
807
  wp.api.collections.Users = BaseCollection.extend(
808
  /** @lends Users.prototype */
809
  {
810
- url: WP_API_Settings.root + '/users',
811
 
812
  model: wp.api.models.User
813
  }
814
  );
815
 
816
  /**
817
- * Backbone post statuses collection
818
  */
819
  wp.api.collections.PostStatuses = BaseCollection.extend(
820
  /** @lends PostStatuses.prototype */
821
  {
822
- url: WP_API_Settings.root + '/posts/statuses',
 
 
823
 
824
- model: wp.api.models.PostStatus
 
 
 
 
 
 
 
825
 
 
 
826
  }
827
  );
828
 
829
  /**
830
- * Backbone media library collection
831
  */
832
  wp.api.collections.MediaLibrary = BaseCollection.extend(
833
  /** @lends MediaLibrary.prototype */
834
  {
835
- url: WP_API_Settings.root + '/media',
836
 
837
  model: wp.api.models.Media
838
  }
839
  );
840
 
841
  /**
842
- * Backbone taxonomy collection
843
  */
844
  wp.api.collections.Taxonomies = BaseCollection.extend(
845
  /** @lends Taxonomies.prototype */
846
  {
847
  model: wp.api.models.Taxonomy,
848
 
849
- url: WP_API_Settings.root + '/taxonomies'
850
  }
851
  );
852
 
853
  /**
854
- * Backbone comment collection
855
  */
856
  wp.api.collections.Comments = BaseCollection.extend(
857
  /** @lends Comments.prototype */
858
  {
859
  model: wp.api.models.Comment,
860
 
861
- post: null,
862
-
863
- /**
864
- * @class Represent an array of comments
865
- * @augments Backbone.Collection
866
- * @constructs
867
- */
868
- initialize: function( models, options ) {
869
- BaseCollection.prototype.initialize.apply( this, arguments );
870
-
871
- if ( options && options.post ) {
872
- this.post = options.post;
873
- }
874
- },
875
-
876
  /**
877
- * Return URL for collection
878
  *
879
- * @returns {string}
880
  */
881
- url: function() {
882
- return WP_API_Settings.root + '/posts/' + this.post + '/comments';
883
- }
884
  }
885
  );
886
 
887
  /**
888
- * Backbone post type collection
889
  */
890
  wp.api.collections.PostTypes = BaseCollection.extend(
891
  /** @lends PostTypes.prototype */
892
  {
893
  model: wp.api.models.PostType,
894
 
895
- url: WP_API_Settings.root + '/posts/types'
 
 
 
 
 
 
 
 
 
 
 
 
896
  }
897
  );
898
 
899
  /**
900
- * Backbone terms collection
 
 
901
  */
902
  wp.api.collections.Terms = BaseCollection.extend(
903
  /** @lends Terms.prototype */
904
  {
905
  model: wp.api.models.Term,
906
 
907
- type: 'post',
908
-
909
  taxonomy: 'category',
910
 
911
  /**
912
- * @class Represent an array of terms
913
- * @augments Backbone.Collection
914
  * @constructs
915
  */
916
  initialize: function( models, options ) {
917
- BaseCollection.prototype.initialize.apply( this, arguments );
918
-
919
- if ( typeof options !== 'undefined' ) {
920
- if ( options.type ) {
921
- this.type = options.type;
922
- }
923
-
924
- if ( options.taxonomy ) {
925
- this.taxonomy = options.taxonomy;
926
- }
927
  }
928
 
929
- this.on( 'add', _.bind( this.addModel, this ) );
930
- },
931
-
932
- /**
933
- * We need to set the type and taxonomy for each model
934
- *
935
- * @param {Backbone.model} model
936
- */
937
- addModel: function( model ) {
938
- model.type = this.type;
939
- model.taxonomy = this.taxonomy;
940
  },
941
 
942
  /**
943
- * Return URL for collection
944
  *
945
- * @returns {string}
946
  */
947
  url: function() {
948
- return WP_API_Settings.root + '/posts/types/' + this.type + '/taxonomies/' + this.taxonomy + '/terms/';
949
  }
950
  }
951
  );
952
 
953
  /**
954
- * Backbone revisions collection
 
 
955
  */
956
  wp.api.collections.Revisions = BaseCollection.extend(
957
  /** @lends Revisions.prototype */
@@ -961,8 +948,8 @@
961
  parent: null,
962
 
963
  /**
964
- * @class Represent an array of revisions
965
- * @augments Backbone.Collection
966
  * @constructs
967
  */
968
  initialize: function( models, options ) {
@@ -974,14 +961,22 @@
974
  },
975
 
976
  /**
977
- * return URL for collection
978
  *
979
- * @returns {string}
980
  */
981
  url: function() {
982
- return WP_API_Settings.root + '/posts/' + this.parent + '/revisions';
983
  }
984
  }
985
  );
986
 
987
- })( wp, WP_API_Settings, Backbone, _, window );
 
 
 
 
 
 
 
 
13
 
14
  })( window );
15
 
16
+ (function( window, undefined ) {
17
+
18
+ 'use strict';
19
 
20
+ window.wp = window.wp || {};
21
+ wp.api = wp.api || {};
22
+ wp.api.utils = wp.api.utils || {};
23
 
24
+ /**
25
+ * ECMAScript 5 shim, from MDN.
26
+ * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
27
+ */
28
  if ( ! Date.prototype.toISOString ) {
29
  var pad = function( number ) {
30
  var r = String( number );
31
  if ( r.length === 1 ) {
32
  r = '0' + r;
33
  }
34
+
35
  return r;
36
  };
37
 
42
  'T' + pad( this.getUTCHours() ) +
43
  ':' + pad( this.getUTCMinutes() ) +
44
  ':' + pad( this.getUTCSeconds() ) +
45
+ '.' + String( ( this.getUTCMilliseconds() / 1000 ).toFixed( 3 ) ).slice( 2, 5 ) +
46
  'Z';
47
  };
48
  }
49
 
50
+ /**
51
+ * Parse date into ISO8601 format.
52
+ *
53
+ * @param {Date} date.
54
+ */
55
+ wp.api.utils.parseISO8601 = function( date ) {
56
+ var timestamp, struct, i, k,
57
+ minutesOffset = 0,
58
  numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ];
59
 
60
+ // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string
61
+ // before falling back to any implementation-specific date parsing, so that’s what we do, even if native
62
+ // implementations could be faster.
63
+ // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm
64
+ if ( ( struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec( date ) ) ) {
65
+ // Avoid NaN timestamps caused by “undefined” values being passed to Date.UTC.
66
+ for ( i = 0; ( k = numericKeys[i] ); ++i ) {
67
+ struct[k] = +struct[k] || 0;
68
+ }
69
 
70
+ // Allow undefined days and months.
71
+ struct[2] = ( +struct[2] || 1 ) - 1;
72
+ struct[3] = +struct[3] || 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
+ if ( struct[8] !== 'Z' && struct[9] !== undefined ) {
75
+ minutesOffset = struct[10] * 60 + struct[11];
76
 
77
+ if ( struct[9] === '+' ) {
78
+ minutesOffset = 0 - minutesOffset;
 
79
  }
 
 
 
 
 
80
  }
81
 
82
+ timestamp = Date.UTC( struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7] );
83
+ } else {
84
+ timestamp = Date.parse ? Date.parse( date ) : NaN;
85
+ }
86
 
87
+ return timestamp;
88
+ };
 
89
 
90
+ })( window );
91
 
92
  /* global WP_API_Settings:false */
93
  // Suppress warning about parse function's unused "options" argument:
94
  /* jshint unused:false */
95
+ (function( wp, WP_API_Settings, Backbone, window, undefined ) {
96
 
97
  'use strict';
98
 
99
  /**
100
+ * Array of parseable dates.
101
  *
102
+ * @type {string[]}.
103
  */
104
  var parseable_dates = [ 'date', 'modified', 'date_gmt', 'modified_gmt' ];
105
 
106
  /**
107
+ * Mixin for all content that is time stamped.
108
  *
109
+ * @type {{toJSON: toJSON, parse: parse}}.
110
  */
111
  var TimeStampedMixin = {
112
  /**
113
+ * Serialize the entity pre-sync.
114
  *
115
+ * @returns {*}.
116
  */
117
  toJSON: function() {
118
  var attributes = _.clone( this.attributes );
119
 
120
+ // Serialize Date objects back into 8601 strings.
121
+ _.each( parseable_dates, function( key ) {
122
  if ( key in attributes ) {
123
  attributes[key] = attributes[key].toISOString();
124
  }
128
  },
129
 
130
  /**
131
+ * Unserialize the fetched response.
132
  *
133
+ * @param {*} response.
134
+ * @returns {*}.
135
  */
136
  parse: function( response ) {
137
+
138
+ // Parse dates into native Date objects.
139
  _.each( parseable_dates, function ( key ) {
140
  if ( ! ( key in response ) ) {
141
  return;
145
  response[key] = new Date( timestamp );
146
  });
147
 
148
+ // Parse the author into a User object.
149
+ if ( 'undefined' !== typeof response.author ) {
150
  response.author = new wp.api.models.User( response.author );
151
  }
152
 
155
  };
156
 
157
  /**
158
+ * Mixin for all hierarchical content types such as posts.
159
  *
160
+ * @type {{parent: parent}}.
161
  */
162
  var HierarchicalMixin = {
163
  /**
164
+ * Get parent object.
165
  *
166
  * @returns {Backbone.Model}
167
  */
169
 
170
  var object, parent = this.get( 'parent' );
171
 
172
+ // Return null if we don't have a parent.
173
  if ( parent === 0 ) {
174
  return null;
175
  }
176
 
177
  var parentModel = this;
178
 
179
+ if ( 'undefined' !== typeof this.parentModel ) {
180
  /**
181
  * Probably a better way to do this. Perhaps grab a cached version of the
182
  * instantiated model?
188
  if ( parentModel.collection ) {
189
  return parentModel.collection.get( parent );
190
  } else {
191
+
192
+ // Otherwise, get the object directly.
193
  object = new parentModel.constructor( {
194
+ id: parent
195
  });
196
 
197
+ // Note that this acts asynchronously.
198
  object.fetch();
199
+
200
  return object;
201
  }
202
  }
203
  };
204
 
205
  /**
206
+ * Private Backbone base model for all models.
207
  */
208
+ var WPApiBaseModel = Backbone.Model.extend(
209
+ /** @lends WPApiBaseModel.prototype */
210
  {
211
  /**
212
+ * Set nonce header before every Backbone sync.
213
  *
214
+ * @param {string} method.
215
+ * @param {Backbone.Model} model.
216
+ * @param {{beforeSend}, *} options.
217
+ * @returns {*}.
218
  */
219
  sync: function( method, model, options ) {
220
  options = options || {};
221
 
222
+ if ( 'undefined' !== typeof WP_API_Settings.nonce ) {
223
  var beforeSend = options.beforeSend;
224
 
225
  options.beforeSend = function( xhr ) {
237
  );
238
 
239
  /**
240
+ * Backbone model for a single user.
241
+ *
242
+ *
243
+ * @param {Object} attributes
244
+ * @param {int} attributes.id The user id. Optional. Defaults to 'me', fetching the current user.
245
  */
246
+ wp.api.models.User = WPApiBaseModel.extend(
247
  /** @lends User.prototype */
248
  {
249
+ idAttribute: 'id',
250
 
251
+ urlRoot: WP_API_Settings.root + 'wp/v2/users',
252
 
253
  defaults: {
254
+ id: 'me',
255
+ avatar_url: {},
256
+ capabilities: {},
257
+ description: '',
258
  email: '',
259
+ extra_capabilities: {},
 
260
  first_name: '',
261
  last_name: '',
262
+ link: '',
263
+ name: '',
264
  nickname: '',
265
+ registered_date: new Date(),
266
+ roles: [],
267
  slug: '',
268
+ url: '',
269
+ username: '',
270
+ _links: {}
 
 
 
 
 
 
 
 
 
 
 
 
271
  }
272
  }
273
  );
274
 
275
  /**
276
+ * Model for a single taxonomy.
277
+ *
278
+ * @param {Object} attributes
279
+ * @param {string} attributes.slug The taxonomy slug.
280
  */
281
+ wp.api.models.Taxonomy = WPApiBaseModel.extend(
282
  /** @lends Taxonomy.prototype */
283
  {
284
  idAttribute: 'slug',
285
 
286
+ urlRoot: WP_API_Settings.root + 'wp/v2/taxonomies',
287
 
288
  defaults: {
289
  name: '',
290
  slug: null,
291
+ description: '',
292
  labels: {},
293
+ types: [],
294
  show_cloud: false,
295
+ hierarchical: false
 
 
 
296
  }
297
  }
298
  );
299
 
300
  /**
301
+ * Backbone model for a single term.
302
+ *
303
+ * @param {Object} attributes
304
+ * @param {int} id attributesm id.
305
  */
306
+ wp.api.models.Term = WPApiBaseModel.extend(
307
  /** @lends Term.prototype */
308
  {
309
+ idAttribute: 'id',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
 
311
+ urlRoot: WP_API_Settings.root + 'wp/v2/terms/tag',
 
312
 
313
  defaults: {
314
+ id: null,
315
  name: '',
316
  slug: '',
317
  description: '',
318
  parent: null,
319
  count: 0,
320
  link: '',
321
+ taxonomy: '',
322
+ _links: {}
 
323
  }
324
 
325
+ }
326
  );
327
 
328
  /**
329
+ * Backbone model for a single post.
330
+ *
331
+ * @param {Object} attributes
332
+ * @param {int} attributes.id The post id.
333
  */
334
+ wp.api.models.Post = WPApiBaseModel.extend( _.extend(
335
  /** @lends Post.prototype */
336
  {
337
+ idAttribute: 'id',
338
 
339
+ urlRoot: WP_API_Settings.root + 'wp/v2/posts',
340
 
341
  defaults: {
342
+ id: null,
 
 
 
 
 
 
 
343
  date: new Date(),
344
  date_gmt: new Date(),
345
+ guid: {},
346
+ link: '',
347
  modified: new Date(),
348
  modified_gmt: new Date(),
349
+ password: '',
350
+ status: 'draft',
351
+ type: 'post',
352
+ title: {},
353
+ content: {},
354
+ author: null,
355
+ excerpt: {},
356
+ featured_image: null,
357
  comment_status: 'open',
358
  ping_status: 'open',
359
  sticky: false,
360
+ format: 'standard',
361
+ _links: {}
 
 
 
 
 
 
362
  }
363
  }, TimeStampedMixin, HierarchicalMixin )
364
  );
365
 
366
  /**
367
+ * Backbone model for a single page.
368
+ *
369
+ * @param {Object} attributes
370
+ * @param {int} attributes.id The page id.
371
  */
372
+ wp.api.models.Page = WPApiBaseModel.extend( _.extend(
373
  /** @lends Page.prototype */
374
  {
375
+ idAttribute: 'id',
376
 
377
+ urlRoot: WP_API_Settings.root + 'wp/v2/pages',
378
 
379
  defaults: {
380
+ id: null,
 
 
 
 
 
 
 
381
  date: new Date(),
 
382
  date_gmt: new Date(),
383
+ guid: {},
384
+ link: '',
385
+ modified: new Date(),
386
  modified_gmt: new Date(),
 
 
 
 
 
 
 
 
 
 
387
  password: '',
388
+ slug: '',
389
+ status: 'draft',
390
+ type: 'page',
391
+ title: {},
392
+ content: {},
393
+ author: null,
394
+ excerpt: {},
395
  featured_image: null,
396
+ comment_status: 'closed',
397
+ ping_status: 'closed',
398
+ menu_order: null,
399
+ template: '',
400
+ _links: {}
401
  }
402
  }, TimeStampedMixin, HierarchicalMixin )
403
  );
404
 
405
  /**
406
+ * Backbone model for a single post revision.
407
+ *
408
+ * @param {Object} attributes
409
+ * @param {int} attributes.parent The id of the post that this revision belongs to.
410
+ * @param {int} attributes.id The revision id.
411
  */
412
+ wp.api.models.PostRevision = WPApiBaseModel.extend( _.extend(
413
+ /** @lends PostRevision.prototype */
414
  {
415
+ idAttribute: 'id',
 
 
 
 
 
 
 
 
 
 
416
 
417
+ defaults: {
418
+ id: null,
419
+ author: null,
420
+ date: new Date(),
421
+ date_gmt: new Date(),
422
+ guid: {},
423
+ modified: new Date(),
424
+ modified_gmt: new Date(),
425
+ parent: 0,
426
+ slug: '',
427
+ title: {},
428
+ content: {},
429
+ excerpt: {},
430
+ _links: {}
431
  },
432
 
433
  /**
434
+ * Return URL for the model.
435
+ *
436
+ * @returns {string}.
437
  */
438
+ url: function() {
439
+ var id = this.get( 'id' ) || '',
440
+ parent = this.get( 'parent' ) || '';
441
+
442
+ return WP_API_Settings.root + 'wp/v2/posts/' + parent + '/revisions/' + id;
443
  }
444
+
445
+ }, TimeStampedMixin, HierarchicalMixin )
446
  );
447
 
448
  /**
449
+ * Backbone model for a single media item.
450
+ *
451
+ * @param {Object} attributes
452
+ * @param {int} attributes.id The media item id.
453
  */
454
+ wp.api.models.Media = WPApiBaseModel.extend( _.extend(
455
  /** @lends Media.prototype */
456
  {
457
+ idAttribute: 'id',
458
 
459
+ urlRoot: WP_API_Settings.root + 'wp/v2/media',
460
 
461
  defaults: {
462
+ id: null,
 
 
 
 
 
 
 
463
  date: new Date(),
464
+ date_gmt: new Date(),
465
+ guid: {},
466
+ link: '',
467
  modified: new Date(),
468
+ modified_gmt: new Date(),
469
+ password: '',
470
  slug: '',
471
+ status: 'draft',
472
+ type: 'attachment',
473
+ title: {},
474
+ author: null,
475
  comment_status: 'open',
476
  ping_status: 'open',
477
+ alt_text: '',
478
+ caption: '',
479
+ description: '',
480
+ media_type: '',
481
+ media_details: {},
482
+ post: null,
483
+ source_url: '',
484
+ _links: {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  }
486
+
487
+ }, TimeStampedMixin )
488
  );
489
 
490
  /**
491
+ * Backbone model for a single comment.
492
+ *
493
+ * @param {Object} attributes
494
+ * @param {int} attributes.id The comment id.
495
  */
496
+ wp.api.models.Comment = WPApiBaseModel.extend( _.extend(
497
  /** @lends Comment.prototype */
498
  {
499
+ idAttribute: 'id',
500
+
501
+ urlRoot: WP_API_Settings.root + 'wp/v2/comments',
502
 
503
  defaults: {
504
+ id: null,
505
+ author: null,
506
+ author_email: '',
507
+ author_ip: '',
508
+ author_name: '',
509
+ author_url: '',
510
+ author_user_agent: '',
511
+ content: {},
512
  date: new Date(),
513
  date_gmt: new Date(),
514
+ karma: 0,
515
+ link: '',
516
+ parent: 0,
517
+ status: 'hold',
518
+ type: '',
519
+ _links: {}
 
 
 
 
 
 
 
 
 
 
 
 
 
520
  }
521
+
522
  }, TimeStampedMixin, HierarchicalMixin )
523
  );
524
 
525
  /**
526
+ * Backbone model for a single post type.
527
+ *
528
+ * @param {Object} attributes
529
+ * @param {string} attributes.slug The post type slug.
530
  */
531
+ wp.api.models.PostType = WPApiBaseModel.extend(
532
  /** @lends PostType.prototype */
533
  {
534
  idAttribute: 'slug',
535
 
536
+ urlRoot: WP_API_Settings.root + 'wp/v2/types',
537
 
538
  defaults: {
539
  slug: null,
540
  name: '',
541
  description: '',
542
  labels: {},
543
+ hierarchical: false
 
 
 
 
 
 
544
  },
545
 
546
  /**
547
+ * Prevent model from being saved.
548
  *
549
+ * @returns {boolean}.
550
  */
551
+ save: function() {
552
  return false;
553
  },
554
 
555
  /**
556
+ * Prevent model from being deleted.
557
  *
558
+ * @returns {boolean}.
559
  */
560
+ destroy: function() {
561
  return false;
562
  }
563
  }
564
  );
565
 
566
  /**
567
+ * Backbone model for a a single post status.
568
+ *
569
+ * @param {Object} attributes
570
+ * @param {string} attributes.slug The post status slug.
571
  */
572
+ wp.api.models.PostStatus = WPApiBaseModel.extend(
573
  /** @lends PostStatus.prototype */
574
  {
575
  idAttribute: 'slug',
576
 
577
+ urlRoot: WP_API_Settings.root + 'wp/v2/statuses',
578
 
579
  defaults: {
580
  slug: null,
584
  'private': false,
585
  queryable: true,
586
  show_in_list: true,
587
+ _links: {}
 
 
588
  },
589
 
590
  /**
591
+ * Prevent model from being saved.
592
  *
593
+ * @returns {boolean}.
594
  */
595
  save: function() {
596
  return false;
597
  },
598
 
599
  /**
600
+ * Prevent model from being deleted.
601
  *
602
+ * @returns {boolean}.
603
  */
604
+ destroy: function() {
605
  return false;
606
  }
607
  }
608
  );
609
 
610
+ /**
611
+ * API Schema model. Contains meta information about the API.
612
+ */
613
+ wp.api.models.Schema = WPApiBaseModel.extend(
614
+ /** @lends Shema.prototype */
615
+ {
616
+ url: WP_API_Settings.root + 'wp/v2',
617
+
618
+ defaults: {
619
+ namespace: '',
620
+ _links: '',
621
+ routes: {}
622
+ },
623
+
624
+ /**
625
+ * Prevent model from being saved.
626
+ *
627
+ * @returns {boolean}.
628
+ */
629
+ save: function() {
630
+ return false;
631
+ },
632
+
633
+ /**
634
+ * Prevent model from being deleted.
635
+ *
636
+ * @returns {boolean}.
637
+ */
638
+ destroy: function() {
639
+ return false;
640
+ }
641
+ }
642
+ );
643
+
644
+
645
+ })( wp, WP_API_Settings, Backbone, window );
646
 
647
  /* global WP_API_Settings:false */
648
  (function( wp, WP_API_Settings, Backbone, _, window, undefined ) {
649
 
650
  'use strict';
651
 
652
+ /**
653
+ * Contains basic collection functionality such as pagination.
654
+ */
655
  var BaseCollection = Backbone.Collection.extend(
656
  /** @lends BaseCollection.prototype */
657
  {
658
 
659
  /**
660
+ * Setup default state.
661
  */
662
  initialize: function() {
663
  this.state = {
673
  *
674
  * Set nonce header before every Backbone sync.
675
  *
676
+ * @param {string} method.
677
+ * @param {Backbone.Model} model.
678
+ * @param {{success}, *} options.
679
+ * @returns {*}.
680
  */
681
  sync: function( method, model, options ) {
682
  options = options || {};
683
+ var beforeSend = options.beforeSend,
684
+ self = this;
685
 
686
+ if ( 'undefined' !== typeof WP_API_Settings.nonce ) {
687
  options.beforeSend = function( xhr ) {
688
  xhr.setRequestHeader( 'X-WP-Nonce', WP_API_Settings.nonce );
689
 
690
  if ( beforeSend ) {
691
+ return beforeSend.apply( self, arguments );
692
  }
693
  };
694
  }
695
 
696
  if ( 'read' === method ) {
 
 
697
  if ( options.data ) {
698
+ self.state.data = _.clone( options.data );
699
 
700
+ delete self.state.data.page;
701
  } else {
702
+ self.state.data = options.data = {};
703
  }
704
 
705
+ if ( 'undefined' === typeof options.data.page ) {
706
+ self.state.currentPage = null;
707
+ self.state.totalPages = null;
708
+ self.state.totalObjects = null;
709
  } else {
710
+ self.state.currentPage = options.data.page - 1;
711
  }
712
 
713
  var success = options.success;
714
  options.success = function( data, textStatus, request ) {
715
+ self.state.totalPages = parseInt( request.getResponseHeader( 'x-wp-totalpages' ), 10 );
716
+ self.state.totalObjects = parseInt( request.getResponseHeader( 'x-wp-total' ), 10 );
717
 
718
+ if ( self.state.currentPage === null ) {
719
+ self.state.currentPage = 1;
720
  } else {
721
+ self.state.currentPage++;
722
  }
723
 
724
  if ( success ) {
731
  },
732
 
733
  /**
734
+ * Fetches the next page of objects if a new page exists.
735
  *
736
+ * @param {data: {page}} options.
737
+ * @returns {*}.
738
  */
739
  more: function( options ) {
740
  options = options || {};
742
 
743
  _.extend( options.data, this.state.data );
744
 
745
+ if ( 'undefined' === typeof options.data.page ) {
746
  if ( ! this.hasMore() ) {
747
  return false;
748
  }
758
  },
759
 
760
  /**
761
+ * Returns true if there are more pages of objects available.
762
  *
763
+ * @returns null|boolean.
764
  */
765
  hasMore: function() {
766
  if ( this.state.totalPages === null ||
775
  );
776
 
777
  /**
778
+ * Backbone collection for posts.
779
  */
780
  wp.api.collections.Posts = BaseCollection.extend(
781
  /** @lends Posts.prototype */
782
  {
783
+ url: WP_API_Settings.root + 'wp/v2/posts',
784
 
785
  model: wp.api.models.Post
786
  }
787
  );
788
 
789
  /**
790
+ * Backbone collection for pages.
791
  */
792
  wp.api.collections.Pages = BaseCollection.extend(
793
  /** @lends Pages.prototype */
794
  {
795
+ url: WP_API_Settings.root + 'wp/v2/pages',
796
 
797
  model: wp.api.models.Page
798
  }
799
  );
800
 
801
  /**
802
+ * Backbone users collection.
803
  */
804
  wp.api.collections.Users = BaseCollection.extend(
805
  /** @lends Users.prototype */
806
  {
807
+ url: WP_API_Settings.root + 'wp/v2/users',
808
 
809
  model: wp.api.models.User
810
  }
811
  );
812
 
813
  /**
814
+ * Backbone post statuses collection.
815
  */
816
  wp.api.collections.PostStatuses = BaseCollection.extend(
817
  /** @lends PostStatuses.prototype */
818
  {
819
+ url: WP_API_Settings.root + 'wp/v2/statuses',
820
+
821
+ model: wp.api.models.PostStatus,
822
 
823
+ parse: function( response ) {
824
+ var responseArray = [];
825
+
826
+ for ( var property in response ) {
827
+ if ( response.hasOwnProperty( property ) ) {
828
+ responseArray.push( response[property] );
829
+ }
830
+ }
831
 
832
+ return this.constructor.__super__.parse.call( this, responseArray );
833
+ }
834
  }
835
  );
836
 
837
  /**
838
+ * Backbone media library collection.
839
  */
840
  wp.api.collections.MediaLibrary = BaseCollection.extend(
841
  /** @lends MediaLibrary.prototype */
842
  {
843
+ url: WP_API_Settings.root + 'wp/v2/media',
844
 
845
  model: wp.api.models.Media
846
  }
847
  );
848
 
849
  /**
850
+ * Backbone taxonomy collection.
851
  */
852
  wp.api.collections.Taxonomies = BaseCollection.extend(
853
  /** @lends Taxonomies.prototype */
854
  {
855
  model: wp.api.models.Taxonomy,
856
 
857
+ url: WP_API_Settings.root + 'wp/v2/taxonomies'
858
  }
859
  );
860
 
861
  /**
862
+ * Backbone comment collection.
863
  */
864
  wp.api.collections.Comments = BaseCollection.extend(
865
  /** @lends Comments.prototype */
866
  {
867
  model: wp.api.models.Comment,
868
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
869
  /**
870
+ * Return URL for collection.
871
  *
872
+ * @returns {string}.
873
  */
874
+ url: WP_API_Settings.root + 'wp/v2/comments'
 
 
875
  }
876
  );
877
 
878
  /**
879
+ * Backbone post type collection.
880
  */
881
  wp.api.collections.PostTypes = BaseCollection.extend(
882
  /** @lends PostTypes.prototype */
883
  {
884
  model: wp.api.models.PostType,
885
 
886
+ url: WP_API_Settings.root + 'wp/v2/types',
887
+
888
+ parse: function( response ) {
889
+ var responseArray = [];
890
+
891
+ for ( var property in response ) {
892
+ if ( response.hasOwnProperty( property ) ) {
893
+ responseArray.push( response[property] );
894
+ }
895
+ }
896
+
897
+ return this.constructor.__super__.parse.call( this, responseArray );
898
+ }
899
  }
900
  );
901
 
902
  /**
903
+ * Backbone terms collection.
904
+ *
905
+ * Usage: new wp.api.collections.Terms( {}, { taxonomy: 'taxonomy-slug' } )
906
  */
907
  wp.api.collections.Terms = BaseCollection.extend(
908
  /** @lends Terms.prototype */
909
  {
910
  model: wp.api.models.Term,
911
 
 
 
912
  taxonomy: 'category',
913
 
914
  /**
915
+ * @class Represent an array of terms.
916
+ * @augments Backbone.Collection.
917
  * @constructs
918
  */
919
  initialize: function( models, options ) {
920
+ if ( 'undefined' !== typeof options && options.taxonomy ) {
921
+ this.taxonomy = options.taxonomy;
 
 
 
 
 
 
 
 
922
  }
923
 
924
+ BaseCollection.prototype.initialize.apply( this, arguments );
 
 
 
 
 
 
 
 
 
 
925
  },
926
 
927
  /**
928
+ * Return URL for collection.
929
  *
930
+ * @returns {string}.
931
  */
932
  url: function() {
933
+ return WP_API_Settings.root + 'wp/v2/terms/' + this.taxonomy;
934
  }
935
  }
936
  );
937
 
938
  /**
939
+ * Backbone revisions collection.
940
+ *
941
+ * Usage: new wp.api.collections.Revisions( {}, { parent: POST_ID } ).
942
  */
943
  wp.api.collections.Revisions = BaseCollection.extend(
944
  /** @lends Revisions.prototype */
948
  parent: null,
949
 
950
  /**
951
+ * @class Represent an array of revisions.
952
+ * @augments Backbone.Collection.
953
  * @constructs
954
  */
955
  initialize: function( models, options ) {
961
  },
962
 
963
  /**
964
+ * return URL for collection.
965
  *
966
+ * @returns {string}.
967
  */
968
  url: function() {
969
+ return WP_API_Settings.root + 'wp/v2/posts/' + this.parent + '/revisions';
970
  }
971
  }
972
  );
973
 
974
+ /**
975
+ * Todo: Handle schema endpoints.
976
+ */
977
+
978
+ /**
979
+ * Todo: Handle post meta.
980
+ */
981
+
982
+ })( wp, WP_API_Settings, Backbone, _, window );