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

Version Description

Download this release

Release Info

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

Version 2.0-beta3

CHANGELOG.md ADDED
@@ -0,0 +1,2545 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## 2.0 Beta 3.0
4
+
5
+ - Add ability to declare sanitization and default options for schema fields.
6
+
7
+ The `arg_options` array can be used to declare the sanitization callback,
8
+ default value, or requirement of a field.
9
+
10
+ (props @joehoyle, [#1345][gh-1345])
11
+ (props @joehoyle, [#1346][gh-1346])
12
+
13
+ - Expand supported parameters for creating and updating Comments.
14
+
15
+ (props @rachelbaker, [#1245][gh-1245])
16
+
17
+ - Declare collection parameters for Terms of a Post.
18
+
19
+ Define the available collection parameters in `get_collection_params()` and
20
+ allow Terms of a Post to be queried by term order.
21
+
22
+ (props @danielbachhuber, [#1332][gh-1332])
23
+
24
+ - Improve the Attachment error message for an invalid Content-Disposition
25
+
26
+ (props @danielbachhuber, [#1317][gh-1317])
27
+
28
+ - Return 200 status when updating Attachments, Comments, and Users.
29
+
30
+ (props @rachelbaker, [#1348][gh-1348])
31
+
32
+ - Remove unnecessary `handle_format_param()` method.
33
+
34
+ (props @danielbachhuber, [#1331][gh-1331])
35
+
36
+ - Add `author_avatar_url` field to the Comment response and schema.
37
+
38
+ (props @rachelbaker [#1327][gh-1327])
39
+
40
+ - Introduce `rest_do_request()` for making REST requests internally.
41
+
42
+ (props @danielbachhuber, [#1333][gh-1333])
43
+
44
+ - Remove unused DateTime class.
45
+
46
+ (props @rmccue, [#1314][gh-1314])
47
+
48
+ - Add inline documentation for `$wp_rest_server` global.
49
+
50
+ (props @Shelob9, [#1324][gh-1324])
51
+
52
+ [View all changes](https://github.com/WP-API/WP-API/compare/2.0-beta2...2.0-beta3)
53
+ [gh-1245]: https://github.com/WP-API/WP-API/issues/1245
54
+ [gh-1314]: https://github.com/WP-API/WP-API/issues/1314
55
+ [gh-1317]: https://github.com/WP-API/WP-API/issues/1317
56
+ [gh-1318]: https://github.com/WP-API/WP-API/issues/1318
57
+ [gh-1324]: https://github.com/WP-API/WP-API/issues/1324
58
+ [gh-1326]: https://github.com/WP-API/WP-API/issues/1326
59
+ [gh-1327]: https://github.com/WP-API/WP-API/issues/1327
60
+ [gh-1331]: https://github.com/WP-API/WP-API/issues/1331
61
+ [gh-1332]: https://github.com/WP-API/WP-API/issues/1332
62
+ [gh-1333]: https://github.com/WP-API/WP-API/issues/1333
63
+ [gh-1345]: https://github.com/WP-API/WP-API/issues/1345
64
+ [gh-1346]: https://github.com/WP-API/WP-API/issues/1346
65
+ [gh-1347]: https://github.com/WP-API/WP-API/issues/1347
66
+ [gh-1348]: https://github.com/WP-API/WP-API/issues/1348
67
+
68
+ ## 2.0 Beta 2.0
69
+
70
+ - Load the WP REST API before the main query runs.
71
+
72
+ The `rest_api_loaded` function now hooks into the `parse_request` action.
73
+ This change prevents the main query from being run on every request and
74
+ allows sites to set `WP_USE_THEMES` to `false`. Previously, the main query
75
+ was always being run (`SELECT * FROM wp_posts LIMIT 10`), even though the
76
+ result was never used and couldn't be cached.
77
+
78
+ (props @rmccue, [#1270][gh-1270])
79
+
80
+ - Register a new field on an existing WordPress object type.
81
+
82
+ Introduces `register_api_field()` to add a field to an object and
83
+ its schema.
84
+
85
+ (props @joehoyle, @rachelbaker, [#927][gh-927])
86
+ (props @joehoyle, [#1207][gh-1207])
87
+ (props @joehoyle, [#1243][gh-1243])
88
+
89
+ - Add endpoints for viewing, creating, updating, and deleting Terms for a Post.
90
+
91
+ The new `WP_REST_Posts_Terms_Controller` class controller supports routes for
92
+ Terms that belong to a Post.
93
+
94
+ (props @joehoyle, @danielbachhuber, [#1216][gh-1216])
95
+
96
+ - Add pagination headers for collection queries.
97
+
98
+ The `X-WP-Total` and `X-WP-TotalPages` are now present in terms, comments,
99
+ and users collection responses.
100
+
101
+ (props @danielbachhuber, [#1182][gh-1182])
102
+ (props @danielbachhuber, [#1191][gh-1191])
103
+ (props @danielbachhuber, @joehoyle, [#1197][gh-1197])
104
+
105
+ - List registered namespaces in the index for feature detection.
106
+
107
+ The index (`/wp-json` by default) now contains a list of the available
108
+ namespaces. This allows for simple feature detection. You can grab the index
109
+ and check namespaces for `wp/v3` or `pluginname/v2`, which indicate the
110
+ supported endpoints on the site.
111
+
112
+ (props @rmccue,, [#1283][gh-1283])
113
+
114
+ - Standardize link property relations and support embedding for all resources.
115
+
116
+ Change link properties to use IANA-registered relations. Also adds embedding
117
+ support to Attachments, Comments and Terms.
118
+
119
+ (props @rmccue, @rachelbaker, [#1284][gh-1284])
120
+
121
+ - Add support for Composer dependency management.
122
+
123
+ Allows you to recursively install/update the WP REST API inside of WordPress
124
+ plugins or themes.
125
+
126
+ (props @QWp6t, [#1157][gh-1157])
127
+
128
+ - Return full objects in the delete response.
129
+
130
+ Instead of returning a random message when deleting a Post, Comment, Term, or
131
+ User provide the original resource data.
132
+
133
+ (props @danielbachhuber, [#1253][gh-1253])
134
+ (props @danielbachhuber, [#1254][gh-1254])
135
+ (props @danielbachhuber, [#1255][gh-1255])
136
+ (props @danielbachhuber, [#1256][gh-1256])
137
+
138
+ - Return programmatically readable error messages for invalid or missing
139
+ required parameters.
140
+
141
+ (props @joehoyle, [#1175][gh-1175])
142
+
143
+ - Declare supported arguments for Comment and User collection queries.
144
+
145
+ (props @danielbachhuber, [#1211][gh-1211])
146
+ (props @danielbachhuber, [#1217][gh-1217])
147
+
148
+ - Automatically validate parameters based on Schema data.
149
+
150
+ (props @joehoyle, [#1128][gh-1128])
151
+
152
+ - Use the `show_in_rest` attributes for exposing Taxonomies.
153
+
154
+ (props @joehoyle, [#1279][gh-1279])
155
+
156
+ - Handle `parent` when creating or updating a Term.
157
+
158
+ (props @joehoyle, [#1221][gh-1221])
159
+
160
+ - Limit fields returned in `embed` context User responses.
161
+
162
+ (props @rachelbaker, [#1251][gh-1251])
163
+
164
+ - Only include `parent` in term response when tax is hierarchical.
165
+
166
+ (props @danielbachhuber, [#1189][gh-1189])
167
+
168
+ - Fix bug in creating comments if `type` was not set.
169
+
170
+ (props @rachelbaker, [#1244][gh-1244])
171
+
172
+ - Rename `post_name` field to `post_slug`.
173
+
174
+ (props @danielbachhuber, [#1235][gh-1235])
175
+
176
+ - Add check when creating a user to verify the provided role is valid.
177
+
178
+ (props @rachelbaker, [#1267][gh-1267])
179
+
180
+ - Add link properties to the Post Status response.
181
+
182
+ (props @joehoyle, [#1243][gh-1243])
183
+
184
+ - Return `0` for `parent` in Post response instead of `null`.
185
+
186
+ (props @danielbachhuber, [#1269][gh-1269])
187
+
188
+ - Only link `author` when there's a valid author
189
+
190
+ (props @danielbachhuber, [#1203][gh-1203])
191
+
192
+ - Only permit querying by parent term when tax is hierarchical.
193
+
194
+ (props @danielbachhuber, [#1219][gh-1219])
195
+
196
+ - Only permit deleting posts of the proper type
197
+
198
+ (props @danielbachhuber, [#1257][gh-1257])
199
+
200
+ - Set pagination headers even when no found posts.
201
+
202
+ (props @danielbachhuber, [#1209][gh-1209])
203
+
204
+ - Correct prefix in `rest_request_parameter_order` filter.
205
+
206
+ (props @quasel, [#1158][gh-1158])
207
+
208
+ - Retool `WP_REST_Terms_Controller` to follow Posts controller pattern.
209
+
210
+ (props @danielbachhuber, [#1170][gh-1170])
211
+
212
+ - Remove unused `accept_json argument` from the `register_routes` method.
213
+
214
+ (props @quasel, [#1160][gh-1160])
215
+
216
+ - Fix typo in `sanitize_params` inline documentation.
217
+
218
+ (props @Shelob9, [#1226][gh-1226])
219
+
220
+ - Remove commented out code in dispatch method.
221
+
222
+ (props @rachelbaker, [#1162][gh-1162])
223
+
224
+
225
+ [View all changes](https://github.com/WP-API/WP-API/compare/2.0-beta1.1...2.0-beta2)
226
+ [gh-927]: https://github.com/WP-API/WP-API/issues/927
227
+ [gh-1128]: https://github.com/WP-API/WP-API/issues/1128
228
+ [gh-1157]: https://github.com/WP-API/WP-API/issues/1157
229
+ [gh-1158]: https://github.com/WP-API/WP-API/issues/1158
230
+ [gh-1160]: https://github.com/WP-API/WP-API/issues/1160
231
+ [gh-1162]: https://github.com/WP-API/WP-API/issues/1162
232
+ [gh-1168]: https://github.com/WP-API/WP-API/issues/1168
233
+ [gh-1170]: https://github.com/WP-API/WP-API/issues/1170
234
+ [gh-1171]: https://github.com/WP-API/WP-API/issues/1171
235
+ [gh-1175]: https://github.com/WP-API/WP-API/issues/1175
236
+ [gh-1176]: https://github.com/WP-API/WP-API/issues/1176
237
+ [gh-1177]: https://github.com/WP-API/WP-API/issues/1177
238
+ [gh-1181]: https://github.com/WP-API/WP-API/issues/1181
239
+ [gh-1182]: https://github.com/WP-API/WP-API/issues/1182
240
+ [gh-1188]: https://github.com/WP-API/WP-API/issues/1188
241
+ [gh-1189]: https://github.com/WP-API/WP-API/issues/1189
242
+ [gh-1191]: https://github.com/WP-API/WP-API/issues/1191
243
+ [gh-1197]: https://github.com/WP-API/WP-API/issues/1197
244
+ [gh-1200]: https://github.com/WP-API/WP-API/issues/1200
245
+ [gh-1203]: https://github.com/WP-API/WP-API/issues/1203
246
+ [gh-1207]: https://github.com/WP-API/WP-API/issues/1207
247
+ [gh-1209]: https://github.com/WP-API/WP-API/issues/1209
248
+ [gh-1210]: https://github.com/WP-API/WP-API/issues/1210
249
+ [gh-1211]: https://github.com/WP-API/WP-API/issues/1211
250
+ [gh-1216]: https://github.com/WP-API/WP-API/issues/1216
251
+ [gh-1217]: https://github.com/WP-API/WP-API/issues/1217
252
+ [gh-1219]: https://github.com/WP-API/WP-API/issues/1219
253
+ [gh-1221]: https://github.com/WP-API/WP-API/issues/1221
254
+ [gh-1226]: https://github.com/WP-API/WP-API/issues/1226
255
+ [gh-1235]: https://github.com/WP-API/WP-API/issues/1235
256
+ [gh-1243]: https://github.com/WP-API/WP-API/issues/1243
257
+ [gh-1244]: https://github.com/WP-API/WP-API/issues/1244
258
+ [gh-1249]: https://github.com/WP-API/WP-API/issues/1249
259
+ [gh-1251]: https://github.com/WP-API/WP-API/issues/1251
260
+ [gh-1253]: https://github.com/WP-API/WP-API/issues/1253
261
+ [gh-1254]: https://github.com/WP-API/WP-API/issues/1254
262
+ [gh-1255]: https://github.com/WP-API/WP-API/issues/1255
263
+ [gh-1256]: https://github.com/WP-API/WP-API/issues/1256
264
+ [gh-1257]: https://github.com/WP-API/WP-API/issues/1257
265
+ [gh-1259]: https://github.com/WP-API/WP-API/issues/1259
266
+ [gh-1267]: https://github.com/WP-API/WP-API/issues/1267
267
+ [gh-1268]: https://github.com/WP-API/WP-API/issues/1268
268
+ [gh-1269]: https://github.com/WP-API/WP-API/issues/1269
269
+ [gh-1270]: https://github.com/WP-API/WP-API/issues/1270
270
+ [gh-1276]: https://github.com/WP-API/WP-API/issues/1276
271
+ [gh-1277]: https://github.com/WP-API/WP-API/issues/1277
272
+ [gh-1279]: https://github.com/WP-API/WP-API/issues/1279
273
+ [gh-1283]: https://github.com/WP-API/WP-API/issues/1283
274
+ [gh-1284]: https://github.com/WP-API/WP-API/issues/1284
275
+ [gh-1295]: https://github.com/WP-API/WP-API/issues/1295
276
+ [gh-1301]: https://github.com/WP-API/WP-API/issues/1301
277
+
278
+
279
+ ## 2.0 Beta 1.1
280
+
281
+ - Fix user access security vulnerability.
282
+
283
+ Authenticated users were able to escalate their privileges bypassing the
284
+ expected capabilities check.
285
+
286
+ Reported by @kacperszurek on 2015-05-16.
287
+
288
+
289
+ ## 2.0 Beta 1
290
+
291
+ - Avoid passing server to the controller each time
292
+
293
+ (props @rmccue, [#543][gh-543])
294
+
295
+ - Unify naming of methods across classes
296
+
297
+ (props @danielbachhuber, [#546][gh-546])
298
+
299
+ - Disable unit tests while we move things around
300
+
301
+ (props @danielbachhuber, [#548][gh-548])
302
+
303
+ - Mock code to represent new Resources
304
+
305
+ (props @danielbachhuber, [#549][gh-549])
306
+
307
+ - WP_JSON_Controller POC
308
+
309
+ (props @danielbachhuber, [#556][gh-556])
310
+
311
+ - Add request object
312
+
313
+ (props @rmccue, [#563][gh-563])
314
+
315
+ - Update routes for new-style registration
316
+
317
+ (props @rmccue, [#564][gh-564])
318
+
319
+ - Add compatibility with v1 routing
320
+
321
+ (props @rmccue, [#565][gh-565])
322
+
323
+ - Remove Last-Modified and If-Unmodified-Since
324
+
325
+ (props @rmccue, [#566][gh-566])
326
+
327
+ - Allow multiple route registration
328
+
329
+ (props @rmccue, [#586][gh-586])
330
+
331
+ - Use https in test setup
332
+
333
+ (props @danielbachhuber, [#588][gh-588])
334
+
335
+ - Terms Controller Redux
336
+
337
+ (props @danielbachhuber, [#579][gh-579])
338
+
339
+ - Add hypermedia functionality to the response
340
+
341
+ (props @rmccue, @rachelbaker, [#570][gh-570])
342
+
343
+ - Initial pass at new style Users Controller
344
+
345
+ (props @rachelbaker, [#603][gh-603])
346
+
347
+ - Drop old Users class
348
+
349
+ (props @danielbachhuber, [#619][gh-619])
350
+
351
+ - Fix passing array to 'methods' are in register_json_route()
352
+
353
+ (props @joehoyle, [#620][gh-620])
354
+
355
+ - Allow 'ignore_sticky_posts' filter #415
356
+
357
+ (props @Shelob9, [#612][gh-612], [#415][gh-415])
358
+
359
+ - Initial Extras.php commit
360
+
361
+ (props @NikV, [#575][gh-575])
362
+
363
+ - Allow filtering response before returning
364
+
365
+ (props @danielbachhuber, [#573][gh-573])
366
+
367
+ - Parse JSON data from the request
368
+
369
+ (props @rmccue, [#626][gh-626])
370
+
371
+ - Remove old taxonomies controller
372
+
373
+ (props @danielbachhuber, [#637][gh-637])
374
+
375
+ - Make our code DRY by consolidating use of strtoupper
376
+
377
+ (props @danielbachhuber, [#589][gh-589])
378
+
379
+ - Move WP_Test_JSON_Testcase to a properly named file
380
+
381
+ (props @danielbachhuber, [#643][gh-643])
382
+
383
+ - Speed up builds by only running against MS once
384
+
385
+ (props @danielbachhuber, [#638][gh-638])
386
+
387
+ - `->prepare_post()` should be public
388
+
389
+ (props @staylor, [#645][gh-645])
390
+
391
+ - Get by and return `term_taxonomy_id`
392
+
393
+ (props @danielbachhuber, [#648][gh-648])
394
+
395
+ - Base class with standard test methods for every controller
396
+
397
+ (props @danielbachhuber, [#649][gh-649])
398
+
399
+ - Unused arguments
400
+
401
+ (props @staylor, [#647][gh-647])
402
+
403
+ - JS should be under version control
404
+
405
+ (props @staylor, [#644][gh-644])
406
+
407
+ - Register multiple routes for users correctly
408
+
409
+ (props @rmccue, [#654][gh-654])
410
+
411
+ - Check get_post_type_object() returns an object before using it
412
+
413
+ (props @NateWr, [#656][gh-656])
414
+
415
+ - Run multisite test against PHP 5.2
416
+
417
+ (props @danielbachhuber, [#659][gh-659])
418
+
419
+ - Pass the edit context when returning the create or update response. Fixes
420
+ #661
421
+
422
+ (props @rachelbaker, [#664][gh-664], [#661][gh-661])
423
+
424
+ - Check for errors when responding to create
425
+
426
+ (props @rmccue, [#652][gh-652])
427
+
428
+ - Fix bug in check_required_parameters where JSON params were missed
429
+
430
+ (props @rachelbaker, [#673][gh-673])
431
+
432
+ - Fix parameter handling and improve Users Controller tests
433
+
434
+ (props @rachelbaker, [#675][gh-675])
435
+
436
+ - Check that param is null
437
+
438
+ (props @danielbachhuber, [#678][gh-678])
439
+
440
+ - Parse URL-encoded body with PUT requests
441
+
442
+ (props @rmccue, [#681][gh-681])
443
+
444
+ - End to end testing for users
445
+
446
+ (props @rmccue, [#682][gh-682])
447
+
448
+ - End to end test coverage of Terms Controller
449
+
450
+ (props @danielbachhuber, @rmccue, [#676][gh-676])
451
+
452
+ - Add ability to wrap response in an envelope
453
+
454
+ (props @Japh, @rmccue, [#628][gh-628])
455
+
456
+ - Wrap up PUT handling in Users Controller
457
+
458
+ (props @rachelbaker, [#683][gh-683])
459
+
460
+ - ID shouldn't be a param on update user endpoint
461
+
462
+ (props @joehoyle, [#692][gh-692])
463
+
464
+ - Clean up Terms controller
465
+
466
+ (props @danielbachhuber, [#696][gh-696])
467
+
468
+ - Remove mis-placed duplicate Users Delete route and id parameter
469
+
470
+ (props @rachelbaker, [#700][gh-700])
471
+
472
+ - Fields cleanup for User controller
473
+
474
+ (props @danielbachhuber, [#701][gh-701])
475
+
476
+ - Throw an error when a user tries to update to an existing user's email
477
+
478
+ (props @danielbachhuber, [#705][gh-705])
479
+
480
+ - `PUT User` shouldn't permit using existing `user_login` or `user_nicename`
481
+
482
+ (props @danielbachhuber, [#707][gh-707])
483
+
484
+ - Change return value of WP_JSON_Users_Controller::get_item.
485
+
486
+ (props @rachelbaker, [#712][gh-712])
487
+
488
+ - Add the ability to specify default param values in register_json_route
489
+
490
+ (props @WP-API, [#715][gh-715])
491
+
492
+ - Merge JS into main repo
493
+
494
+ (props @tlovett1, [#730][gh-730])
495
+
496
+ - Make the "required" param on args optional
497
+
498
+ (props @joehoyle, @rachelbaker, [#728][gh-728])
499
+
500
+ - Always allow JSON data for POST and PUT requests
501
+
502
+ (props @rachelbaker, [#731][gh-731])
503
+
504
+ - Initial pass at new style Posts Controller
505
+
506
+ (props @rachelbaker, [#684][gh-684])
507
+
508
+ - Drop required argument declaration
509
+
510
+ (props @danielbachhuber, [#736][gh-736])
511
+
512
+ - Update post format after post has been updated
513
+
514
+ (props @danielbachhuber, [#737][gh-737])
515
+
516
+ - Allow the title to be set via title.raw
517
+
518
+ (props @iseulde, [#741][gh-741])
519
+
520
+ - Fix some incompatible interfaces
521
+
522
+ (props @staylor, [#742][gh-742])
523
+
524
+ - Full Test Coverage for Users Controller
525
+
526
+ (props @rachelbaker, [#744][gh-744])
527
+
528
+ - Refer to BaseCollection statically instead of via this.constructor
529
+
530
+ (props @tlovett1, [#750][gh-750])
531
+
532
+ - Adjustments to Users Controller DocBlocks
533
+
534
+ (props @rachelbaker, [#743][gh-743])
535
+
536
+ - Default `args` to an empty array
537
+
538
+ (props @danielbachhuber, [#758][gh-758])
539
+
540
+ - Do not require type parameter to be set when updating a Post
541
+
542
+ (props @rachelbaker, [#761][gh-761])
543
+
544
+ - Remove from docs the "post_type" filter parameter for /posts endpoint
545
+
546
+ (props @NateWr, [#666][gh-666])
547
+
548
+ - Resolve regressions in Posts Controller
549
+
550
+ (props @rachelbaker, [#753][gh-753])
551
+
552
+ - WP_Json_Server::dispatch() should always return a WP_JSON_Response
553
+
554
+ (props @joehoyle, [#714][gh-714])
555
+
556
+ - Update Timeline note
557
+
558
+ (props @tapsboy, [#774][gh-774])
559
+
560
+ - Make json_pre_dispatch and json_post_dispatch consistent
561
+
562
+ (props @joehoyle, [#786][gh-786])
563
+
564
+ - Normalize our test classes setUP and tearDown methods
565
+
566
+ (props @rachelbaker, [#794][gh-794])
567
+
568
+ - Comments Endpoints
569
+
570
+ (props @joehoyle, @rachelbaker, [#693][gh-693])
571
+
572
+ - Correct /posts/ endpoint read post permission logic
573
+
574
+ (props @rachelbaker, [#805][gh-805])
575
+
576
+ - Ensure global $post has proper state when the json_prepare_post filter f...
577
+
578
+ (props @ericandrewlewis, [#823][gh-823])
579
+
580
+ - Adds missing description field to the Taxonomy response
581
+
582
+ (props @rachelbaker, [#826][gh-826])
583
+
584
+ - Posts controller abstraction
585
+
586
+ (props @danielbachhuber, [#820][gh-820])
587
+
588
+ - Remove old Pages and CustomPostType classes no longer in use
589
+
590
+ (props @danielbachhuber, [#831][gh-831])
591
+
592
+ - Add `featured_image` attribute for post types that support `thumbnails`
593
+
594
+ (props @danielbachhuber, [#832][gh-832])
595
+
596
+ - Specify Capability in Route
597
+
598
+ (props @joehoyle, [#602][gh-602])
599
+
600
+ - Posts Controller Headers and Links Fixes
601
+
602
+ (props @rachelbaker, [#836][gh-836])
603
+
604
+ - Don't noop `future` status. It's confusing
605
+
606
+ (props @danielbachhuber, [#841][gh-841])
607
+
608
+ - Remove unused $request parameter from prepare_links method.
609
+
610
+ (props @rachelbaker, [#842][gh-842])
611
+
612
+ - Expose basic author details when user has published posts
613
+
614
+ (props @danielbachhuber, [#838][gh-838])
615
+
616
+ - Make `get_post_type_base()` public so we can DRY
617
+
618
+ (props @danielbachhuber, [#845][gh-845])
619
+
620
+ - Remove Duplicate Logic for Post Type Attributes
621
+
622
+ (props @rachelbaker, [#853][gh-853])
623
+
624
+ - Move infrastructure classes to `lib/infrastructure`, part one
625
+
626
+ (props @danielbachhuber, [#872][gh-872])
627
+
628
+ - Passing a value for the slug parameter should update the post_name.
629
+
630
+ (props @rachelbaker, [#883][gh-883])
631
+
632
+ - Break Pages tests into a separate class
633
+
634
+ (props @danielbachhuber, [#870][gh-870])
635
+
636
+ - Empty checks in Posts Controller make setting values to Falsy impossible
637
+
638
+ (props @joehoyle, [#885][gh-885])
639
+
640
+ - Change project name to WP REST API in plugin name and Readme title.
641
+
642
+ (props @rachelbaker, [#876][gh-876])
643
+
644
+ - Return 200 and an empty array for valid queries with 0 results.
645
+
646
+ (props @rachelbaker, [#888][gh-888])
647
+
648
+ - Include the taxonomy in the term response
649
+
650
+ (props @danielbachhuber, [#891][gh-891])
651
+
652
+ - JSON Schemas for our Controllers, second attempt
653
+
654
+ (props @danielbachhuber, [#844][gh-844])
655
+
656
+ - From the left with love
657
+
658
+ (props @MichaelArestad, [#896][gh-896])
659
+
660
+ - Add `link` field to Users, Comments and Terms
661
+
662
+ (props @danielbachhuber, [#897][gh-897])
663
+
664
+ - Fix flipped assertions
665
+
666
+ (props @danielbachhuber, [#902][gh-902])
667
+
668
+ - Add missing break statement
669
+
670
+ (props @danielbachhuber, [#905][gh-905])
671
+
672
+ - Move all of our endpoint controllers to `lib/endpoints`
673
+
674
+ (props @danielbachhuber, [#906][gh-906])
675
+
676
+ - Always include `guid` in Post and Page schemas
677
+
678
+ (props @danielbachhuber, [#907][gh-907])
679
+
680
+ - If post type doesn't match controller post type, throw 404
681
+
682
+ (props @danielbachhuber, [#908][gh-908])
683
+
684
+ - Allow post type attributes to be set based on presence in schema
685
+
686
+ (props @danielbachhuber, [#910][gh-910])
687
+
688
+ - Updating another post field shouldn't change sticky status
689
+
690
+ (props @danielbachhuber, [#911][gh-911])
691
+
692
+ - Expose post type data at `/types`
693
+
694
+ (props @danielbachhuber, [#914][gh-914])
695
+
696
+ - Always defer to controller for post type
697
+
698
+ (props @danielbachhuber, [#913][gh-913])
699
+
700
+ - Add `template` parameter to Page response
701
+
702
+ (props @danielbachhuber, [#909][gh-909])
703
+
704
+ - Convert /media to new controller pattern
705
+
706
+ (props @danielbachhuber, [#904][gh-904])
707
+
708
+ - Remove v1.0 Posts (and Media) controller
709
+
710
+ (props @WP-API, [#923][gh-923])
711
+
712
+ - Clean up taxonomies controller tests by running through dispatch; add schema
713
+
714
+ (props @danielbachhuber, [#919][gh-919])
715
+
716
+ - Separate permissions logic for comments
717
+
718
+ (props @joehoyle, [#854][gh-854])
719
+
720
+ - `wp-json.php` isn't needed anymore
721
+
722
+ (props @danielbachhuber, [#931][gh-931])
723
+
724
+ - Tweak the post controller
725
+
726
+ (props @rmccue, [#936][gh-936])
727
+
728
+ - Switch CORS headers callback to new action
729
+
730
+ (props @rmccue, [#935][gh-935])
731
+
732
+ - Remove `_id` suffix from field names
733
+
734
+ (props @danielbachhuber, [#941][gh-941])
735
+
736
+ - Add `author_ip`, `author_user_agent` and `karma` fields to Comment
737
+
738
+ (props @danielbachhuber, [#946][gh-946])
739
+
740
+ - Explicitly test that these additional comment fields aren't present
741
+
742
+ (props @danielbachhuber, [#947][gh-947])
743
+
744
+ - Allow `title` to be set to empty string in request
745
+
746
+ (props @danielbachhuber, [#953][gh-953])
747
+
748
+ - Use real URLs instead of query_params attribute
749
+
750
+ (props @rmccue, [#958][gh-958])
751
+
752
+ - Use `wp_filter_post_kses()` instead of `wp_kses_post()` on insert
753
+
754
+ (props @danielbachhuber, [#917][gh-917])
755
+
756
+ - Add missing core path to post endpoint link hrefs.
757
+
758
+ (props @rachelbaker, [#966][gh-966])
759
+
760
+ - Allow HTTP method to be overwritten by HTTP_X_HTTP_METHOD_OVERRIDE
761
+
762
+ (props @tlovett1, [#967][gh-967])
763
+
764
+ - Fix attachment caption and description fields
765
+
766
+ (props @danielbachhuber, [#968][gh-968])
767
+
768
+ - Move validation to the `WP_JSON_Request` class
769
+
770
+ (props @danielbachhuber, [#971][gh-971])
771
+
772
+ - Move the Route Registering to the Controllers
773
+
774
+ (props @joehoyle, [#970][gh-970])
775
+
776
+ - Correct test method spelling of permission.
777
+
778
+ (props @rachelbaker, [#973][gh-973])
779
+
780
+ - Permission abstractions 2
781
+
782
+ (props @joehoyle, [#987][gh-987])
783
+
784
+ - If an invalid date is supplied to create / update post, return an error
785
+
786
+ (props @joehoyle, [#1000][gh-1000])
787
+
788
+ - Update README.md
789
+
790
+ (props @hubdotcom, [#1006][gh-1006])
791
+
792
+ - Add embeddable attachments to Post response _links
793
+
794
+ (props @rachelbaker, [#1026][gh-1026])
795
+
796
+ - Throw error if requesting user doesn't have capability for context
797
+
798
+ (props @danielbachhuber, [#1033][gh-1033])
799
+
800
+ - `/wp/statuses` endpoint, modeled after `/wp/types`
801
+
802
+ (props @danielbachhuber, [#1039][gh-1039])
803
+
804
+ - Turn post types from array to object, with name as key
805
+
806
+ (props @danielbachhuber, [#1042][gh-1042])
807
+
808
+ - Add missing response fields to the user schema.
809
+
810
+ (props @rachelbaker, [#1034][gh-1034])
811
+
812
+ - Setting a post to be sticky AND password protected should fail
813
+
814
+ (props @joehoyle, [#1044][gh-1044])
815
+
816
+ - Use appropriate functions when creating users on multisite
817
+
818
+ (props @danielbachhuber, [#1043][gh-1043])
819
+
820
+ - Define context in which each schema field appears
821
+
822
+ (props @danielbachhuber, [#1046][gh-1046])
823
+
824
+ - Use schema abstraction to limit which user fields are exposed per context
825
+
826
+ (props @danielbachhuber, [#1049][gh-1049])
827
+
828
+ - Run Statuses, Types, and Taxonomies through our context filter
829
+
830
+ (props @danielbachhuber, [#1050][gh-1050])
831
+
832
+ - Run Terms controller through schema context filter
833
+
834
+ (props @danielbachhuber, [#1051][gh-1051])
835
+
836
+ - Don't allow contributors to set sticky on posts
837
+
838
+ (props @joehoyle, [#1052][gh-1052])
839
+
840
+ - Return correct response code from wp_insert_post() error
841
+
842
+ (props @joehoyle, [#999][gh-999])
843
+
844
+ - Move the permissions checks for password and author into the permissions
845
+ callback
846
+
847
+ (props @joehoyle, [#1054][gh-1054])
848
+
849
+ - Use full Post schema to filter fields based on context
850
+
851
+ (props @danielbachhuber, [#1053][gh-1053])
852
+
853
+ - Allow WP_JSON_Server::send_header()/send_headers() to be accessed publicly
854
+
855
+ (props @johnbillion, [#1059][gh-1059])
856
+
857
+ - Remove unnecessary sticky posts abstraction
858
+
859
+ (props @danielbachhuber, [#1064][gh-1064])
860
+
861
+ - Re-enable the Post endpoint filters
862
+
863
+ (props @rachelbaker, [#1028][gh-1028])
864
+
865
+ - Fix the format of the args when building them from the Schema
866
+
867
+ (props @joehoyle, [#1066][gh-1066])
868
+
869
+ - Add more tests for the server class
870
+
871
+ (props @rmccue, [#685][gh-685])
872
+
873
+ - Fix error with OPTIONS requests
874
+
875
+ (props @rmccue, [#1091][gh-1091])
876
+
877
+ - Ensure the JSON endpoint URL is properly escaped
878
+
879
+ (props @johnbillion, [#1097][gh-1097])
880
+
881
+ - Correct a bunch of filter docs in WP_JSON_Server
882
+
883
+ (props @johnbillion, [#1098][gh-1098])
884
+
885
+ - Require `moderate_comments` capability to context=edit a Comment
886
+
887
+ (props @danielbachhuber, @joehoyle, [#951][gh-951])
888
+
889
+ - Add all the permission check functions to the base controller for better
890
+ consistancy and help to subclasses
891
+
892
+ (props @joehoyle, [#1104][gh-1104])
893
+
894
+ - `author` is the Comment attribute with user ID
895
+
896
+ (props @danielbachhuber, [#1106][gh-1106])
897
+
898
+ - Fix copy pasta in the schema checks
899
+
900
+ (props @danielbachhuber, [#1111][gh-1111])
901
+
902
+ - When `context=edit`, confirm user can `manage_comments`
903
+
904
+ (props @danielbachhuber, [#1112][gh-1112])
905
+
906
+ - Abstract revisions to dedicated controller; only include revisioned fields
907
+
908
+ (props @danielbachhuber, [#1110][gh-1110])
909
+
910
+ - Add Embeddable Taxonomy Term Links to the Post Response
911
+
912
+ (props @rachelbaker, [#1048][gh-1048])
913
+
914
+ - Increase Terms Controller test coverage
915
+
916
+ (props @rachelbaker, [#1117][gh-1117])
917
+
918
+ - Rename the `wp_json_server_before_serve` to `wp_json_init`
919
+
920
+ (props @joehoyle, [#1105][gh-1105])
921
+
922
+ - Drop revision embedding from posts controller; link instead
923
+
924
+ (props @danielbachhuber, [#1121][gh-1121])
925
+
926
+ - Add security section to our README
927
+
928
+ (props @rmccue, [#1123][gh-1123])
929
+
930
+ - Missing @param inline docs in main plugin file.
931
+
932
+ (props @Shelob9, [#1122][gh-1122])
933
+
934
+ - Ensure post deletion is idempotent
935
+
936
+ (props @rmccue, [#959][gh-959])
937
+
938
+ - Support for validation / sanitize callbacks in arguments
939
+
940
+ (props @joehoyle, [#989][gh-989])
941
+
942
+ - Display links in collections
943
+
944
+ (props @rmccue, @rachelbaker, [#937][gh-937])
945
+
946
+ - Sanitize args using new args API
947
+
948
+ (props @joehoyle, [#1129][gh-1129])
949
+
950
+ - Use the user fields from the item schema as the request args in route
951
+ registration
952
+
953
+ (props @joehoyle, [#1109][gh-1109])
954
+
955
+ - Build the array of args for /wp/posts from the allowed query vars
956
+
957
+ (props @joehoyle, [#1108][gh-1108])
958
+
959
+ - Show all the invalid param errors at once
960
+
961
+ (props @joehoyle, [#1131][gh-1131])
962
+
963
+ - Readonly attribute in schema to exclude from args array
964
+
965
+ (props @joehoyle, [#1133][gh-1133])
966
+
967
+ - Use the `required` flags from the schema for CREATE post
968
+
969
+ (props @joehoyle, [#1132][gh-1132])
970
+
971
+ - Only return 201 on Create. Update should be 200
972
+
973
+ (props @danielbachhuber, [#1142][gh-1142])
974
+
975
+ - Convert meta endpoints to new-style
976
+
977
+ (props @rmccue, @rachelbaker, [#960][gh-960])
978
+
979
+ - Specific error codes for permissions failures
980
+
981
+ (props @joehoyle, [#1148][gh-1148])
982
+
983
+ [View all changes](https://github.com/WP-API/WP-API/compare/1.2.1...2.0-beta1)
984
+ [gh-347]: https://github.com/WP-API/WP-API/issues/347
985
+ [gh-378]: https://github.com/WP-API/WP-API/issues/378
986
+ [gh-401]: https://github.com/WP-API/WP-API/issues/401
987
+ [gh-415]: https://github.com/WP-API/WP-API/issues/415
988
+ [gh-448]: https://github.com/WP-API/WP-API/issues/448
989
+ [gh-474]: https://github.com/WP-API/WP-API/issues/474
990
+ [gh-481]: https://github.com/WP-API/WP-API/issues/481
991
+ [gh-524]: https://github.com/WP-API/WP-API/issues/524
992
+ [gh-528]: https://github.com/WP-API/WP-API/issues/528
993
+ [gh-543]: https://github.com/WP-API/WP-API/issues/543
994
+ [gh-546]: https://github.com/WP-API/WP-API/issues/546
995
+ [gh-548]: https://github.com/WP-API/WP-API/issues/548
996
+ [gh-549]: https://github.com/WP-API/WP-API/issues/549
997
+ [gh-550]: https://github.com/WP-API/WP-API/issues/550
998
+ [gh-556]: https://github.com/WP-API/WP-API/issues/556
999
+ [gh-563]: https://github.com/WP-API/WP-API/issues/563
1000
+ [gh-564]: https://github.com/WP-API/WP-API/issues/564
1001
+ [gh-565]: https://github.com/WP-API/WP-API/issues/565
1002
+ [gh-566]: https://github.com/WP-API/WP-API/issues/566
1003
+ [gh-567]: https://github.com/WP-API/WP-API/issues/567
1004
+ [gh-570]: https://github.com/WP-API/WP-API/issues/570
1005
+ [gh-573]: https://github.com/WP-API/WP-API/issues/573
1006
+ [gh-575]: https://github.com/WP-API/WP-API/issues/575
1007
+ [gh-579]: https://github.com/WP-API/WP-API/issues/579
1008
+ [gh-586]: https://github.com/WP-API/WP-API/issues/586
1009
+ [gh-588]: https://github.com/WP-API/WP-API/issues/588
1010
+ [gh-589]: https://github.com/WP-API/WP-API/issues/589
1011
+ [gh-591]: https://github.com/WP-API/WP-API/issues/591
1012
+ [gh-595]: https://github.com/WP-API/WP-API/issues/595
1013
+ [gh-602]: https://github.com/WP-API/WP-API/issues/602
1014
+ [gh-603]: https://github.com/WP-API/WP-API/issues/603
1015
+ [gh-612]: https://github.com/WP-API/WP-API/issues/612
1016
+ [gh-619]: https://github.com/WP-API/WP-API/issues/619
1017
+ [gh-620]: https://github.com/WP-API/WP-API/issues/620
1018
+ [gh-626]: https://github.com/WP-API/WP-API/issues/626
1019
+ [gh-628]: https://github.com/WP-API/WP-API/issues/628
1020
+ [gh-630]: https://github.com/WP-API/WP-API/issues/630
1021
+ [gh-637]: https://github.com/WP-API/WP-API/issues/637
1022
+ [gh-638]: https://github.com/WP-API/WP-API/issues/638
1023
+ [gh-643]: https://github.com/WP-API/WP-API/issues/643
1024
+ [gh-644]: https://github.com/WP-API/WP-API/issues/644
1025
+ [gh-645]: https://github.com/WP-API/WP-API/issues/645
1026
+ [gh-647]: https://github.com/WP-API/WP-API/issues/647
1027
+ [gh-648]: https://github.com/WP-API/WP-API/issues/648
1028
+ [gh-649]: https://github.com/WP-API/WP-API/issues/649
1029
+ [gh-652]: https://github.com/WP-API/WP-API/issues/652
1030
+ [gh-654]: https://github.com/WP-API/WP-API/issues/654
1031
+ [gh-656]: https://github.com/WP-API/WP-API/issues/656
1032
+ [gh-659]: https://github.com/WP-API/WP-API/issues/659
1033
+ [gh-661]: https://github.com/WP-API/WP-API/issues/661
1034
+ [gh-664]: https://github.com/WP-API/WP-API/issues/664
1035
+ [gh-666]: https://github.com/WP-API/WP-API/issues/666
1036
+ [gh-673]: https://github.com/WP-API/WP-API/issues/673
1037
+ [gh-675]: https://github.com/WP-API/WP-API/issues/675
1038
+ [gh-676]: https://github.com/WP-API/WP-API/issues/676
1039
+ [gh-678]: https://github.com/WP-API/WP-API/issues/678
1040
+ [gh-681]: https://github.com/WP-API/WP-API/issues/681
1041
+ [gh-682]: https://github.com/WP-API/WP-API/issues/682
1042
+ [gh-683]: https://github.com/WP-API/WP-API/issues/683
1043
+ [gh-684]: https://github.com/WP-API/WP-API/issues/684
1044
+ [gh-685]: https://github.com/WP-API/WP-API/issues/685
1045
+ [gh-692]: https://github.com/WP-API/WP-API/issues/692
1046
+ [gh-693]: https://github.com/WP-API/WP-API/issues/693
1047
+ [gh-696]: https://github.com/WP-API/WP-API/issues/696
1048
+ [gh-700]: https://github.com/WP-API/WP-API/issues/700
1049
+ [gh-701]: https://github.com/WP-API/WP-API/issues/701
1050
+ [gh-705]: https://github.com/WP-API/WP-API/issues/705
1051
+ [gh-707]: https://github.com/WP-API/WP-API/issues/707
1052
+ [gh-712]: https://github.com/WP-API/WP-API/issues/712
1053
+ [gh-714]: https://github.com/WP-API/WP-API/issues/714
1054
+ [gh-715]: https://github.com/WP-API/WP-API/issues/715
1055
+ [gh-722]: https://github.com/WP-API/WP-API/issues/722
1056
+ [gh-728]: https://github.com/WP-API/WP-API/issues/728
1057
+ [gh-730]: https://github.com/WP-API/WP-API/issues/730
1058
+ [gh-731]: https://github.com/WP-API/WP-API/issues/731
1059
+ [gh-736]: https://github.com/WP-API/WP-API/issues/736
1060
+ [gh-737]: https://github.com/WP-API/WP-API/issues/737
1061
+ [gh-741]: https://github.com/WP-API/WP-API/issues/741
1062
+ [gh-742]: https://github.com/WP-API/WP-API/issues/742
1063
+ [gh-743]: https://github.com/WP-API/WP-API/issues/743
1064
+ [gh-744]: https://github.com/WP-API/WP-API/issues/744
1065
+ [gh-750]: https://github.com/WP-API/WP-API/issues/750
1066
+ [gh-753]: https://github.com/WP-API/WP-API/issues/753
1067
+ [gh-758]: https://github.com/WP-API/WP-API/issues/758
1068
+ [gh-761]: https://github.com/WP-API/WP-API/issues/761
1069
+ [gh-774]: https://github.com/WP-API/WP-API/issues/774
1070
+ [gh-786]: https://github.com/WP-API/WP-API/issues/786
1071
+ [gh-794]: https://github.com/WP-API/WP-API/issues/794
1072
+ [gh-805]: https://github.com/WP-API/WP-API/issues/805
1073
+ [gh-807]: https://github.com/WP-API/WP-API/issues/807
1074
+ [gh-815]: https://github.com/WP-API/WP-API/issues/815
1075
+ [gh-820]: https://github.com/WP-API/WP-API/issues/820
1076
+ [gh-823]: https://github.com/WP-API/WP-API/issues/823
1077
+ [gh-826]: https://github.com/WP-API/WP-API/issues/826
1078
+ [gh-831]: https://github.com/WP-API/WP-API/issues/831
1079
+ [gh-832]: https://github.com/WP-API/WP-API/issues/832
1080
+ [gh-836]: https://github.com/WP-API/WP-API/issues/836
1081
+ [gh-838]: https://github.com/WP-API/WP-API/issues/838
1082
+ [gh-841]: https://github.com/WP-API/WP-API/issues/841
1083
+ [gh-842]: https://github.com/WP-API/WP-API/issues/842
1084
+ [gh-844]: https://github.com/WP-API/WP-API/issues/844
1085
+ [gh-845]: https://github.com/WP-API/WP-API/issues/845
1086
+ [gh-849]: https://github.com/WP-API/WP-API/issues/849
1087
+ [gh-853]: https://github.com/WP-API/WP-API/issues/853
1088
+ [gh-854]: https://github.com/WP-API/WP-API/issues/854
1089
+ [gh-870]: https://github.com/WP-API/WP-API/issues/870
1090
+ [gh-872]: https://github.com/WP-API/WP-API/issues/872
1091
+ [gh-874]: https://github.com/WP-API/WP-API/issues/874
1092
+ [gh-876]: https://github.com/WP-API/WP-API/issues/876
1093
+ [gh-879]: https://github.com/WP-API/WP-API/issues/879
1094
+ [gh-883]: https://github.com/WP-API/WP-API/issues/883
1095
+ [gh-885]: https://github.com/WP-API/WP-API/issues/885
1096
+ [gh-888]: https://github.com/WP-API/WP-API/issues/888
1097
+ [gh-891]: https://github.com/WP-API/WP-API/issues/891
1098
+ [gh-896]: https://github.com/WP-API/WP-API/issues/896
1099
+ [gh-897]: https://github.com/WP-API/WP-API/issues/897
1100
+ [gh-902]: https://github.com/WP-API/WP-API/issues/902
1101
+ [gh-904]: https://github.com/WP-API/WP-API/issues/904
1102
+ [gh-905]: https://github.com/WP-API/WP-API/issues/905
1103
+ [gh-906]: https://github.com/WP-API/WP-API/issues/906
1104
+ [gh-907]: https://github.com/WP-API/WP-API/issues/907
1105
+ [gh-908]: https://github.com/WP-API/WP-API/issues/908
1106
+ [gh-909]: https://github.com/WP-API/WP-API/issues/909
1107
+ [gh-910]: https://github.com/WP-API/WP-API/issues/910
1108
+ [gh-911]: https://github.com/WP-API/WP-API/issues/911
1109
+ [gh-913]: https://github.com/WP-API/WP-API/issues/913
1110
+ [gh-914]: https://github.com/WP-API/WP-API/issues/914
1111
+ [gh-917]: https://github.com/WP-API/WP-API/issues/917
1112
+ [gh-919]: https://github.com/WP-API/WP-API/issues/919
1113
+ [gh-923]: https://github.com/WP-API/WP-API/issues/923
1114
+ [gh-931]: https://github.com/WP-API/WP-API/issues/931
1115
+ [gh-933]: https://github.com/WP-API/WP-API/issues/933
1116
+ [gh-935]: https://github.com/WP-API/WP-API/issues/935
1117
+ [gh-936]: https://github.com/WP-API/WP-API/issues/936
1118
+ [gh-937]: https://github.com/WP-API/WP-API/issues/937
1119
+ [gh-941]: https://github.com/WP-API/WP-API/issues/941
1120
+ [gh-946]: https://github.com/WP-API/WP-API/issues/946
1121
+ [gh-947]: https://github.com/WP-API/WP-API/issues/947
1122
+ [gh-951]: https://github.com/WP-API/WP-API/issues/951
1123
+ [gh-953]: https://github.com/WP-API/WP-API/issues/953
1124
+ [gh-955]: https://github.com/WP-API/WP-API/issues/955
1125
+ [gh-958]: https://github.com/WP-API/WP-API/issues/958
1126
+ [gh-959]: https://github.com/WP-API/WP-API/issues/959
1127
+ [gh-960]: https://github.com/WP-API/WP-API/issues/960
1128
+ [gh-966]: https://github.com/WP-API/WP-API/issues/966
1129
+ [gh-967]: https://github.com/WP-API/WP-API/issues/967
1130
+ [gh-968]: https://github.com/WP-API/WP-API/issues/968
1131
+ [gh-970]: https://github.com/WP-API/WP-API/issues/970
1132
+ [gh-971]: https://github.com/WP-API/WP-API/issues/971
1133
+ [gh-973]: https://github.com/WP-API/WP-API/issues/973
1134
+ [gh-985]: https://github.com/WP-API/WP-API/issues/985
1135
+ [gh-987]: https://github.com/WP-API/WP-API/issues/987
1136
+ [gh-989]: https://github.com/WP-API/WP-API/issues/989
1137
+ [gh-996]: https://github.com/WP-API/WP-API/issues/996
1138
+ [gh-999]: https://github.com/WP-API/WP-API/issues/999
1139
+ [gh-1000]: https://github.com/WP-API/WP-API/issues/1000
1140
+ [gh-1006]: https://github.com/WP-API/WP-API/issues/1006
1141
+ [gh-1026]: https://github.com/WP-API/WP-API/issues/1026
1142
+ [gh-1028]: https://github.com/WP-API/WP-API/issues/1028
1143
+ [gh-1033]: https://github.com/WP-API/WP-API/issues/1033
1144
+ [gh-1034]: https://github.com/WP-API/WP-API/issues/1034
1145
+ [gh-1039]: https://github.com/WP-API/WP-API/issues/1039
1146
+ [gh-1042]: https://github.com/WP-API/WP-API/issues/1042
1147
+ [gh-1043]: https://github.com/WP-API/WP-API/issues/1043
1148
+ [gh-1044]: https://github.com/WP-API/WP-API/issues/1044
1149
+ [gh-1046]: https://github.com/WP-API/WP-API/issues/1046
1150
+ [gh-1048]: https://github.com/WP-API/WP-API/issues/1048
1151
+ [gh-1049]: https://github.com/WP-API/WP-API/issues/1049
1152
+ [gh-1050]: https://github.com/WP-API/WP-API/issues/1050
1153
+ [gh-1051]: https://github.com/WP-API/WP-API/issues/1051
1154
+ [gh-1052]: https://github.com/WP-API/WP-API/issues/1052
1155
+ [gh-1053]: https://github.com/WP-API/WP-API/issues/1053
1156
+ [gh-1054]: https://github.com/WP-API/WP-API/issues/1054
1157
+ [gh-1059]: https://github.com/WP-API/WP-API/issues/1059
1158
+ [gh-1064]: https://github.com/WP-API/WP-API/issues/1064
1159
+ [gh-1066]: https://github.com/WP-API/WP-API/issues/1066
1160
+ [gh-1091]: https://github.com/WP-API/WP-API/issues/1091
1161
+ [gh-1097]: https://github.com/WP-API/WP-API/issues/1097
1162
+ [gh-1098]: https://github.com/WP-API/WP-API/issues/1098
1163
+ [gh-1103]: https://github.com/WP-API/WP-API/issues/1103
1164
+ [gh-1104]: https://github.com/WP-API/WP-API/issues/1104
1165
+ [gh-1105]: https://github.com/WP-API/WP-API/issues/1105
1166
+ [gh-1106]: https://github.com/WP-API/WP-API/issues/1106
1167
+ [gh-1108]: https://github.com/WP-API/WP-API/issues/1108
1168
+ [gh-1109]: https://github.com/WP-API/WP-API/issues/1109
1169
+ [gh-1110]: https://github.com/WP-API/WP-API/issues/1110
1170
+ [gh-1111]: https://github.com/WP-API/WP-API/issues/1111
1171
+ [gh-1112]: https://github.com/WP-API/WP-API/issues/1112
1172
+ [gh-1115]: https://github.com/WP-API/WP-API/issues/1115
1173
+ [gh-1116]: https://github.com/WP-API/WP-API/issues/1116
1174
+ [gh-1117]: https://github.com/WP-API/WP-API/issues/1117
1175
+ [gh-1121]: https://github.com/WP-API/WP-API/issues/1121
1176
+ [gh-1122]: https://github.com/WP-API/WP-API/issues/1122
1177
+ [gh-1123]: https://github.com/WP-API/WP-API/issues/1123
1178
+ [gh-1129]: https://github.com/WP-API/WP-API/issues/1129
1179
+ [gh-1131]: https://github.com/WP-API/WP-API/issues/1131
1180
+ [gh-1132]: https://github.com/WP-API/WP-API/issues/1132
1181
+ [gh-1133]: https://github.com/WP-API/WP-API/issues/1133
1182
+ [gh-1134]: https://github.com/WP-API/WP-API/issues/1134
1183
+ [gh-1137]: https://github.com/WP-API/WP-API/issues/1137
1184
+ [gh-1142]: https://github.com/WP-API/WP-API/issues/1142
1185
+ [gh-1148]: https://github.com/WP-API/WP-API/issues/1148
1186
+
1187
+ ## 1.2.1
1188
+
1189
+ - Fix information disclosure security vulnerability.
1190
+
1191
+ Unauthenticated users could access revisions of published and unpublished posts. Revisions are now only accessible to authenticated users with permission to edit the revision's post.
1192
+
1193
+ Reported by @chredd on 2015-04-09.
1194
+
1195
+ ## 1.2.0
1196
+
1197
+ - Add handling for Cross-Origin Resource Sharing (CORS) OPTIONS requests.
1198
+
1199
+ Preflighted requests (using the OPTIONS method) include the headers
1200
+ `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, and
1201
+ `Access-Control-Allow-Credentials` in the response, if the HTTP origin is
1202
+ set.
1203
+
1204
+ (props @rmccue, [#281][gh-281])
1205
+
1206
+ - Allow overriding full requests.
1207
+
1208
+ The `json_pre_dispatch` filter allows a request to be hijacked before it is
1209
+ dispatched. Hijacked requests can be anything a normal endpoint can return.
1210
+
1211
+ (props @rmccue, [#281][gh-281])
1212
+
1213
+ - Check for JSON encoding/decoding errors.
1214
+
1215
+ Returns the last error (if any) occurred during the last JSON encoding or
1216
+ decoding operation.
1217
+
1218
+ (props @joshkadis, @rmccue, [#461][gh-461])
1219
+
1220
+ - Add filtering to the terms collection endpoint.
1221
+
1222
+ Available filter arguments are based on the `get_terms()` function. Example:
1223
+ `/taxonomies/category/terms?filter[number]=10` would limit the response to 10
1224
+ category terms.
1225
+
1226
+ (props @mauteri, [#401][gh-401], [#347][gh-347])
1227
+
1228
+ - Add handling for the `role` parameter when creating or updating a user.
1229
+
1230
+ Allow users to be created or updated with a provided `role`.
1231
+
1232
+ (props @pippinsplugins, [#392][gh-392], [#335][gh-335])
1233
+
1234
+ - Add handling for the `post_id` parameter when creating media.
1235
+
1236
+ Allow passing the `post_id` parameter to associate a new media item with
1237
+ a post.
1238
+
1239
+ (props @pkevan, [#294][gh-294])
1240
+
1241
+ - Handle route matching for `-` in taxonomy and terms.
1242
+
1243
+ Previously the regular expression used to match taxonomy and term names did
1244
+ not support names with dashes.
1245
+
1246
+ (props @EdHurtig, @evansobkowicz, [#410][gh-410])
1247
+
1248
+ - Handle JSONP callback matching for `.` in the function name.
1249
+
1250
+ Previously the regular expression used to match JSONP callback functions did
1251
+ not support names with periods.
1252
+
1253
+ (props @codonnell822, [#455][gh-455])
1254
+
1255
+ - Fix the Content-Type header for JSONP requests.
1256
+
1257
+ Previously JSONP requests sent the incorrect `application/json` Content-Type
1258
+ header with the response. This would result in an error if strict MIME
1259
+ checking was enabled. The Content-Type header was corrected to
1260
+ `application/javascript` for JSONP responses.
1261
+
1262
+ (props @simonlampen, [#380][gh-380])
1263
+
1264
+ - Add `$context` parameter to `json_prepare_term` filter.
1265
+
1266
+ Terms responses can now be modified based on the `context` parameter of the
1267
+ request.
1268
+
1269
+ (props @traversal, [#316][gh-316])
1270
+
1271
+ - Move the JavaScript client library into the plugin.
1272
+
1273
+ Previously, the `wp-api.js` file was a separate repository. The JavaScript
1274
+ client has moved back into the plugin to coordinate code changes.
1275
+
1276
+ (props @tlovett1, [#730][gh-730])
1277
+
1278
+ - Always return an object for media sizes
1279
+
1280
+ The media sizes value should always be an object even when empty. Previously,
1281
+ if a media item did not have any sizes set, an empty array was returned.
1282
+
1283
+ **Compatibility warning**: Clients should be prepared to accept an empty
1284
+ object as a value for media sizes.
1285
+
1286
+ (props @maxcutler, [#300][gh-300])
1287
+
1288
+ - Give top-level posts a `null` parent value.
1289
+
1290
+ For date type consistency, post parent property should be `null`. Previously,
1291
+ parent-less posts returned `0` for parent.
1292
+
1293
+ **Compatibility warning**: Clients should be prepared to accept `null` as a
1294
+ value for post parent.
1295
+
1296
+ (props @maxcutler, [#391][gh-391])
1297
+
1298
+ - Move permission checks out of `WP_JSON_Posts`.
1299
+
1300
+ Introduce `json_check_post_permission()` function to allow post object
1301
+ capability checks to be used outside the `WP_JSON_Posts` class.
1302
+
1303
+ **Deprecation warning:** Calling `WP_JSON_Posts::check_read_permission` and
1304
+ `WP_JSON_Posts::check_edit_permission` is now deprecated.
1305
+
1306
+ (props @rachelbaker, [#486][gh-486], [#378][gh-378])
1307
+
1308
+ - Split comment endpoints into separate class.
1309
+
1310
+ All comment handling has moved to the `WP_JSON_Comments` class.
1311
+
1312
+ **Deprecation warning:** Calling `WP_JSON_Posts::get_comments`,
1313
+ `WP_JSON_Posts::get_comment`, `WP_JSON_Posts::delete_comment`, and
1314
+ `WP_JSON_Posts::prepare_comment` is now deprecated.
1315
+
1316
+ (props @whyisjake, @rmccue, @rachelbaker, [#378][gh-378])
1317
+
1318
+ - Split meta endpoints into separate class.
1319
+
1320
+ All post meta handling has moved to the new `WP_JSON_Meta_Posts` class.
1321
+
1322
+ **Deprecation warning:** Calling `WP_JSON_Posts::get_all_meta`,
1323
+ `WP_JSON_Posts::get_meta`, `WP_JSON_Posts::update_meta`,
1324
+ `WP_JSON_Posts::add_meta`, `WP_JSON_Posts::delete_meta`,
1325
+ `WP_JSON_Posts::prepare_meta`, and `WP_JSON_Posts::is_valid_meta_data` is
1326
+ now deprecated.
1327
+
1328
+ (props @rmccue, @rachelbaker, [#358][gh-358], [#474][gh-474])
1329
+
1330
+ - Rename internal create methods.
1331
+
1332
+ **Deprecation warning:** Calling `WP_JSON_Posts::new_post`,
1333
+ `WP_JSON_CustomPostType::new_post` and `WP_JSON_Posts::new_post`
1334
+ is now deprecated.
1335
+
1336
+ (props @rachelbaker, @rmccue, [#374][gh-374], [#377][gh-377], [#376][gh-376])
1337
+
1338
+ - Fix discrepancies in edit and create posts documentation examples.
1339
+
1340
+ Corrected the edit and create posts code examples in the Getting Started
1341
+ section. The new post example was updated to include the required
1342
+ `content_raw` parameter. The new and edit posts examples were updated to use
1343
+ a correct date parameter.
1344
+
1345
+ (props @rachelbaker, [#305][gh-305])
1346
+
1347
+ - Update the cookie authentication documentation examples.
1348
+
1349
+ With 1.1 the localized JavaScript object for `wp-api.js` changed to
1350
+ `WP_API_Settings`. This updates the Authentication section documentation
1351
+ nonce example to use the updated object name.
1352
+
1353
+ (props @rachelbaker, [#321][gh-321])
1354
+
1355
+ - Add flexibility and multisite support to unit tests.
1356
+
1357
+ Tests can be run from any WordPress install, and are not limited to only as
1358
+ a plugin installed within a WordPress.org develop checkout. Unit tests are
1359
+ now run against a multisite installation.
1360
+
1361
+ (props @danielbachhuber, [#397][gh-397])
1362
+
1363
+ - Add `taxonomy` slug to the term response.
1364
+
1365
+ (props @kalenjohnson, [#481][gh-481])
1366
+
1367
+ - Fix error when getting child comment.
1368
+
1369
+ Previously an error occurred when a requested comment had a parent.
1370
+
1371
+ (props @EdHurtig, [#413][gh-413], [#411][gh-411])
1372
+
1373
+ - Parse query strings before returning a JSON decode error.
1374
+
1375
+ (props @jtsternberg, [#499][gh-499])
1376
+
1377
+ - Typecast the user ID parameter to be an integer for the `/users/{ID}` route.
1378
+
1379
+ (props @dimadin, [#333][gh-333])
1380
+
1381
+ - Confirm a given JSONP callback is a string.
1382
+
1383
+ (props @ircrash, @rmccue, [#405][gh-405])
1384
+
1385
+ - Register the JavaScript client in the admin.
1386
+
1387
+ (props @tlovett1, [#473][gh-473])
1388
+
1389
+ - Remove duplicate error checks on post ids.
1390
+
1391
+ (props @danielbachhuber, [#271][gh-271])
1392
+
1393
+ - Update documentation link references to wp-api.org.
1394
+
1395
+ (props @pollyplummer, [#320][gh-320])
1396
+
1397
+ - Update documentation to note routes needing authentication.
1398
+
1399
+ (props @kellbot, [#402][gh-402], [#309][gh-309])
1400
+
1401
+ - Correct Post route documentation filter parameters.
1402
+
1403
+ (props @modemlooper, @rachelbaker, @rmccue, [#357][gh-357], [#462][gh-462])
1404
+
1405
+ - Update taxonomy route documentation with correct paths.
1406
+
1407
+ (props @davidbhayes, [#364][gh-364], [#355][gh-355])
1408
+
1409
+ - Remove references to legacy `$fields` parameter.
1410
+
1411
+ (props @JDGrimes, [#326][gh-326])
1412
+
1413
+ - Alter readme installation steps to use wp-cli for plugin and permalink setup.
1414
+
1415
+ (props @kadamwhite, [#390][gh-390])
1416
+
1417
+ - Add steps to readme for executing tests with `vagrant ssh -c`.
1418
+
1419
+ (props @kadamwhite, [#416][gh-416])
1420
+
1421
+ - Update readme to include provision step for testing suite.
1422
+
1423
+ (props @ironpaperweight, [#396][gh-396])
1424
+
1425
+ - Update readme Getting Started link.
1426
+
1427
+ (props @NikV, [#519][gh-519])
1428
+
1429
+ - Update readme Chassis repository links.
1430
+
1431
+ (props @Japh, [#505][gh-505])
1432
+
1433
+ - Clean-up of `docs` folder.
1434
+
1435
+ (props @pollyplummer, [#441][gh-441])
1436
+
1437
+ - Documentation audit for plugin.php file.
1438
+
1439
+ (props @DrewAPicture, [#293][gh-293])
1440
+
1441
+ - Rename tests to match class file naming.
1442
+
1443
+ (props @danielbachhuber, @rmccue, [#359][gh-359])
1444
+
1445
+ - Add license.txt file with license terms.
1446
+
1447
+ (props @rachelbaker, [#393][gh-393], [#384][gh-384])
1448
+
1449
+ - Fix test_root when using WordPress.org developer checkout.
1450
+
1451
+ (props @markoheijnen, [#437][gh-437])
1452
+
1453
+ [View all changes](https://github.com/rmccue/WP-API/compare/1.1.1...1.2)
1454
+
1455
+ [gh-271]: https://github.com/WP-API/WP-API/issues/271
1456
+ [gh-281]: https://github.com/WP-API/WP-API/issues/281
1457
+ [gh-293]: https://github.com/WP-API/WP-API/issues/293
1458
+ [gh-294]: https://github.com/WP-API/WP-API/issues/294
1459
+ [gh-300]: https://github.com/WP-API/WP-API/issues/300
1460
+ [gh-305]: https://github.com/WP-API/WP-API/issues/305
1461
+ [gh-309]: https://github.com/WP-API/WP-API/issues/309
1462
+ [gh-316]: https://github.com/WP-API/WP-API/issues/316
1463
+ [gh-320]: https://github.com/WP-API/WP-API/issues/320
1464
+ [gh-321]: https://github.com/WP-API/WP-API/issues/321
1465
+ [gh-326]: https://github.com/WP-API/WP-API/issues/326
1466
+ [gh-333]: https://github.com/WP-API/WP-API/issues/333
1467
+ [gh-333]: https://github.com/WP-API/WP-API/issues/333
1468
+ [gh-335]: https://github.com/WP-API/WP-API/issues/335
1469
+ [gh-347]: https://github.com/WP-API/WP-API/issues/347
1470
+ [gh-355]: https://github.com/WP-API/WP-API/issues/355
1471
+ [gh-357]: https://github.com/WP-API/WP-API/issues/357
1472
+ [gh-358]: https://github.com/WP-API/WP-API/issues/358
1473
+ [gh-359]: https://github.com/WP-API/WP-API/issues/359
1474
+ [gh-364]: https://github.com/WP-API/WP-API/issues/364
1475
+ [gh-374]: https://github.com/WP-API/WP-API/issues/374
1476
+ [gh-376]: https://github.com/WP-API/WP-API/issues/376
1477
+ [gh-377]: https://github.com/WP-API/WP-API/issues/377
1478
+ [gh-378]: https://github.com/WP-API/WP-API/issues/378
1479
+ [gh-380]: https://github.com/WP-API/WP-API/issues/380
1480
+ [gh-384]: https://github.com/WP-API/WP-API/issues/384
1481
+ [gh-390]: https://github.com/WP-API/WP-API/issues/390
1482
+ [gh-391]: https://github.com/WP-API/WP-API/issues/391
1483
+ [gh-392]: https://github.com/WP-API/WP-API/issues/392
1484
+ [gh-393]: https://github.com/WP-API/WP-API/issues/393
1485
+ [gh-396]: https://github.com/WP-API/WP-API/issues/396
1486
+ [gh-397]: https://github.com/WP-API/WP-API/issues/397
1487
+ [gh-401]: https://github.com/WP-API/WP-API/issues/401
1488
+ [gh-402]: https://github.com/WP-API/WP-API/issues/402
1489
+ [gh-405]: https://github.com/WP-API/WP-API/issues/405
1490
+ [gh-410]: https://github.com/WP-API/WP-API/issues/410
1491
+ [gh-411]: https://github.com/WP-API/WP-API/issues/411
1492
+ [gh-413]: https://github.com/WP-API/WP-API/issues/413
1493
+ [gh-416]: https://github.com/WP-API/WP-API/issues/416
1494
+ [gh-437]: https://github.com/WP-API/WP-API/issues/437
1495
+ [gh-438]: https://github.com/WP-API/WP-API/issues/438
1496
+ [gh-441]: https://github.com/WP-API/WP-API/issues/441
1497
+ [gh-455]: https://github.com/WP-API/WP-API/issues/455
1498
+ [gh-458]: https://github.com/WP-API/WP-API/issues/458
1499
+ [gh-461]: https://github.com/WP-API/WP-API/issues/461
1500
+ [gh-462]: https://github.com/WP-API/WP-API/issues/462
1501
+ [gh-473]: https://github.com/WP-API/WP-API/issues/473
1502
+ [gh-474]: https://github.com/WP-API/WP-API/issues/474
1503
+ [gh-481]: https://github.com/WP-API/WP-API/issues/481
1504
+ [gh-486]: https://github.com/WP-API/WP-API/issues/486
1505
+ [gh-499]: https://github.com/WP-API/WP-API/issues/499
1506
+ [gh-505]: https://github.com/WP-API/WP-API/issues/505
1507
+ [gh-519]: https://github.com/WP-API/WP-API/issues/519
1508
+ [gh-524]: https://github.com/WP-API/WP-API/issues/524
1509
+ [gh-528]: https://github.com/WP-API/WP-API/issues/528
1510
+ [gh-595]: https://github.com/WP-API/WP-API/issues/595
1511
+ [gh-730]: https://github.com/WP-API/WP-API/issues/730
1512
+ [gh-933]: https://github.com/WP-API/WP-API/issues/933
1513
+ [gh-985]: https://github.com/WP-API/WP-API/issues/985
1514
+
1515
+ ## 1.1.1
1516
+
1517
+ - Mitigate Flash CSRF exploit
1518
+
1519
+ Using the API's JSONP support, it's possible to control the first bytes of the
1520
+ response sent to the browser. Combining this with an ASCII-encoded SWF allows
1521
+ arbitrary SWFs to be served from the site, allowing bypassing the same-origin
1522
+ policy built in to browsers.
1523
+
1524
+ While the API includes CSRF protection and is not directly vulnerable, this
1525
+ can be used to bypass other browser origin controls.
1526
+
1527
+ Reported by @iandunn on 2014-07-10.
1528
+
1529
+ (props @iandunn, @rmccue, [#356][gh-356])
1530
+
1531
+ [View all changes](https://github.com/rmccue/WP-API/compare/1.0...1.1)
1532
+
1533
+ [gh-356]: https://github.com/WP-API/WP-API/issues/356
1534
+
1535
+ ## 1.1
1536
+
1537
+ - Add new routes for taxonomies and terms.
1538
+
1539
+ Taxonomies and terms have now been moved from the `/posts/types/<type>`
1540
+ namespace to global routes: `/taxonomies`, `/taxonomies/<tax>`,
1541
+ `/taxonomies/<tax>/terms` and `/taxonomies/<tax>/terms/<term>`
1542
+
1543
+ Test coverage for taxonomy endpoints has also been increased to 100%.
1544
+
1545
+ **Deprecation warning**: The `/posts/types/<type>/taxonomies` endpoint (and
1546
+ sub-endpoints with the same prefix) have been deprecated in favour of the new
1547
+ endpoints. These deprecated endpoints will now return a
1548
+ `X-WP-DeprecatedFunction` header indicating that the endpoint should not be
1549
+ used for new development, but will continue to work in the future.
1550
+
1551
+ (props @kadamwhite, @rachelbaker, @rmccue, [#198][gh-198], [#211][gh-211])
1552
+
1553
+ - Allow customizing the API resources prefix
1554
+
1555
+ The API base (typically `wp-json/`) can now be customized to a different
1556
+ prefix using the `json_url_prefix` filter. Note that rewrites will need to be
1557
+ flushed manually after changing this.
1558
+
1559
+ (props @ericandrewlewis, @rmccue, [#104][gh-104], [#244][gh-244], [#278][gh-278])
1560
+
1561
+ - Give `null` as date for draft posts.
1562
+
1563
+ Draft posts would previously return "0000-00-00 00:00:00" or
1564
+ "1970-01-01T00:00:00", as draft posts are not assigned a publish date. The API
1565
+ now returns `null` where a date is not available.
1566
+
1567
+ **Compatibility warning**: Clients should be prepared to accept `null` as a
1568
+ value for date/time fields, and treat it as if no value is set.
1569
+
1570
+ (props @rmccue, [#229][gh-229], [#230][gh-230])
1571
+
1572
+ - Fix errors with excerpt.
1573
+
1574
+ Posts without excerpts could previously return nonsense strings, excerpts from
1575
+ other posts, or cause internal PHP errors. Posts without excerpts will now
1576
+ always return an excerpt, typically automatically generated from the post
1577
+ content.
1578
+
1579
+ The `excerpt_raw` field was added to the edit context on posts. This field
1580
+ contains the raw excerpt data saved for the post, including empty
1581
+ string values.
1582
+
1583
+ (props @rmccue, [#222][gh-226], [#226][gh-226])
1584
+
1585
+ - Only expose email for edit context.
1586
+
1587
+ User email addresses are now only exposed for `context=edit`, which requires
1588
+ the `edit_users` permission (not required for the current user).
1589
+
1590
+ The email address field will now return `false` instead of a string if the
1591
+ field is not exposed.
1592
+
1593
+ (props @pkevan, @rmccue, [#290][gh-290], [#296][gh-296])
1594
+
1595
+ - Correct password-protected post handling.
1596
+
1597
+ Password-protected posts could previously be exposed to all users, however
1598
+ could also have broken behaviour with excerpts. Password-protected posts are
1599
+ now hidden to unauthenticated users, while content and excerpts are shown
1600
+ correctly for the `edit` context.
1601
+
1602
+ (Note that hiding password-protected posts is intended to be a temporary
1603
+ measure, and will likely change in the future.)
1604
+
1605
+ (props @rmccue, [#286][gh-286], [#313][gh-313])
1606
+
1607
+ - Add documentation on authentication methods.
1608
+
1609
+ Full documentation on [authentication](https://github.com/WP-API/WP-API/blob/master/docs/authentication.md)
1610
+ is now available. This documentation explains the difference between the
1611
+ various available authentication methods, and notes which should be used.
1612
+
1613
+ (props @rmccue, [#242][gh-242])
1614
+
1615
+ - Include new client JS from github.io
1616
+
1617
+ The WP-API Javascript library is now loaded dynamically from
1618
+ `wp-api.github.io` to ensure it is always up-to-date.
1619
+
1620
+ (props @tlovett1, [#179][gh-179], [#240][gh-240])
1621
+
1622
+ - Don't allow setting the modification date on post creation/update.
1623
+
1624
+ As it turns out, WP core doesn't allow us to set this, so this was previously
1625
+ a no-op anyway. Discovered during test coverage phase.
1626
+
1627
+ (props @rachelbaker, @rmccue, [#285][gh-285], [#288][gh-288])
1628
+
1629
+ - Check post parent correctly on insertion.
1630
+
1631
+ Posts could previously be added with an invalid parent ID. These IDs are now
1632
+ checked to ensure the post exists.
1633
+
1634
+ (props @rmccue, [#228][gh-228], [#231][gh-231])
1635
+
1636
+ - Make sure the type is actually evaluated for `json_prepare_${type}` filter.
1637
+
1638
+ This value was previously not interpolated correctly, due to the use of the
1639
+ single-quoted string type.
1640
+
1641
+ (props @danielbachhuber, [#266][gh-266])
1642
+
1643
+ - Return `WP_Error` instead of array of empty objects for a revisions
1644
+ permissions error.
1645
+
1646
+ Previously, when trying to access post revisions without correct permissions,
1647
+ a JSON list of internal error objects would be returned. This has been
1648
+ corrected to return a standard API error instead.
1649
+
1650
+ (props @rachelbaker, @tlovett1, [#251][gh-251], [#276][gh-276])
1651
+
1652
+ - Flip user parameters check for insert/update.
1653
+
1654
+ Previously, you could add a user without specifying username/password/email,
1655
+ but couldn't update a user without those parameters. The logic has been
1656
+ inverted here instead.
1657
+
1658
+ (props @rmccue, [#221][gh-221], [#289][gh-289])
1659
+
1660
+ - Add revision endpoints tests
1661
+
1662
+ (props @danielbachhuber, @rachelbaker, @rmccue, [#275][gh-275], [#277][gh-277], [#284][gh-284], [#279][gh-279])
1663
+
1664
+ - Add post endpoint testing
1665
+
1666
+ Now at >54% coverage for the whole class, and >80% for the main methods. This
1667
+ figure will continue to rise over the next few releases.
1668
+
1669
+ (props @rachelbaker, @rmccue, [#99][gh-99])
1670
+
1671
+ - Separate helper functions into global namespace.
1672
+
1673
+ `WP_JSON_Server::get_timezone()`, `WP_JSON_Server::get_date_with_gmt()`,
1674
+ `WP_JSON_Server::get_avatar_url()` and ``WP_JSON_Server::parse_date()` have
1675
+ all been moved into the global namespace to decouple them from the server
1676
+ class.
1677
+
1678
+ **Deprecation warning**: These methods have been deprecated. The new
1679
+ `json_get_timezone()`, `json_get_date_with_gmt()`, `json_get_avatar_url()` and
1680
+ `json_parse_date()` methods should now be used instead.
1681
+
1682
+ (props @rmccue, [#185][gh-185], [#298][gh-298])
1683
+
1684
+ - Re-order Users and Media routes documentation based on CRUD order
1685
+
1686
+ (props @rachelbaker, [#214][gh-214])
1687
+
1688
+ - Update Post route documentation to provide more detail for data parameter
1689
+
1690
+ (props @rachelbaker, [#212][gh-212])
1691
+
1692
+ - Correct documentation typo ("inforcement" -> "enforcement").
1693
+
1694
+ (props @ericandrewlewis, [#236][gh-236])
1695
+
1696
+ - Coding Standards audit
1697
+
1698
+ (props @DrewAPicture, [#235][gh-235])
1699
+
1700
+ - Add comparison documentation.
1701
+
1702
+ (props @rachelbaker, @rmccue, [#217][gh-225], [#225][gh-225])
1703
+
1704
+ - `json_url` filter call should be passed `$scheme`
1705
+
1706
+ (props @ericandrewlewis, [#243][gh-243])
1707
+
1708
+ - Set `class-jsonserializable.php` file mode to 644.
1709
+
1710
+ (props @jeremyfelt, [#255][gh-255])
1711
+
1712
+ - Remove unneeded "which" in implementation doc.
1713
+
1714
+ (props @JDGrimes, [#254][gh-254])
1715
+
1716
+ - Fix a copy/paste error in schema doc.
1717
+
1718
+ (props @JDGrimes, [#253][gh-253])
1719
+
1720
+ - Correct reference link in example schema.
1721
+
1722
+ (props @danielbachhuber, [#258][gh-258])
1723
+
1724
+ - Add missing post formats to post schema documentation.
1725
+
1726
+ (props @danielbachhuber, [#260][gh-260])
1727
+
1728
+ - Ensure we always use "public" on public methods.
1729
+
1730
+ (props @danielbachhuber, [#268][gh-268])
1731
+
1732
+ - Ensure we don't cause a PHP error if a post does not have revisions.
1733
+
1734
+ (props @rmccue, [#227][gh-227])
1735
+
1736
+ - Add note to where upload_files cap comes from
1737
+
1738
+ (props @pkevan, [#282][gh-282])
1739
+
1740
+ - Add handling of `sticky` property when creating or editing posts.
1741
+
1742
+ (props @rachelbaker, [#218][gh-218])
1743
+
1744
+ - Update post route endpoint docs to include details on `post_meta` handling.
1745
+
1746
+ (props @rachelbaker, [#213][gh-213])
1747
+
1748
+ - Update main readme file to better describe the project.
1749
+
1750
+ (props @rmccue, [#303][gh-303])
1751
+
1752
+ - Fix `--data-binary` cURL option in documentation
1753
+
1754
+ (props @Pezzab, @rachelbaker, @rmccue, [#283][gh-283], [#304][gh-304])
1755
+
1756
+ [View all changes](https://github.com/rmccue/WP-API/compare/1.0...1.1)
1757
+
1758
+ [gh-99]: https://github.com/WP-API/WP-API/issues/99
1759
+ [gh-104]: https://github.com/WP-API/WP-API/issues/104
1760
+ [gh-179]: https://github.com/WP-API/WP-API/issues/179
1761
+ [gh-185]: https://github.com/WP-API/WP-API/issues/185
1762
+ [gh-198]: https://github.com/WP-API/WP-API/issues/198
1763
+ [gh-211]: https://github.com/WP-API/WP-API/issues/211
1764
+ [gh-212]: https://github.com/WP-API/WP-API/issues/212
1765
+ [gh-213]: https://github.com/WP-API/WP-API/issues/213
1766
+ [gh-214]: https://github.com/WP-API/WP-API/issues/214
1767
+ [gh-218]: https://github.com/WP-API/WP-API/issues/218
1768
+ [gh-221]: https://github.com/WP-API/WP-API/issues/221
1769
+ [gh-225]: https://github.com/WP-API/WP-API/issues/225
1770
+ [gh-225]: https://github.com/WP-API/WP-API/issues/225
1771
+ [gh-226]: https://github.com/WP-API/WP-API/issues/226
1772
+ [gh-226]: https://github.com/WP-API/WP-API/issues/226
1773
+ [gh-227]: https://github.com/WP-API/WP-API/issues/227
1774
+ [gh-228]: https://github.com/WP-API/WP-API/issues/228
1775
+ [gh-229]: https://github.com/WP-API/WP-API/issues/229
1776
+ [gh-230]: https://github.com/WP-API/WP-API/issues/230
1777
+ [gh-231]: https://github.com/WP-API/WP-API/issues/231
1778
+ [gh-235]: https://github.com/WP-API/WP-API/issues/235
1779
+ [gh-236]: https://github.com/WP-API/WP-API/issues/236
1780
+ [gh-240]: https://github.com/WP-API/WP-API/issues/240
1781
+ [gh-242]: https://github.com/WP-API/WP-API/issues/242
1782
+ [gh-243]: https://github.com/WP-API/WP-API/issues/243
1783
+ [gh-244]: https://github.com/WP-API/WP-API/issues/244
1784
+ [gh-251]: https://github.com/WP-API/WP-API/issues/251
1785
+ [gh-253]: https://github.com/WP-API/WP-API/issues/253
1786
+ [gh-254]: https://github.com/WP-API/WP-API/issues/254
1787
+ [gh-255]: https://github.com/WP-API/WP-API/issues/255
1788
+ [gh-258]: https://github.com/WP-API/WP-API/issues/258
1789
+ [gh-260]: https://github.com/WP-API/WP-API/issues/260
1790
+ [gh-266]: https://github.com/WP-API/WP-API/issues/266
1791
+ [gh-268]: https://github.com/WP-API/WP-API/issues/268
1792
+ [gh-275]: https://github.com/WP-API/WP-API/issues/275
1793
+ [gh-276]: https://github.com/WP-API/WP-API/issues/276
1794
+ [gh-277]: https://github.com/WP-API/WP-API/issues/277
1795
+ [gh-278]: https://github.com/WP-API/WP-API/issues/278
1796
+ [gh-279]: https://github.com/WP-API/WP-API/issues/279
1797
+ [gh-282]: https://github.com/WP-API/WP-API/issues/282
1798
+ [gh-283]: https://github.com/WP-API/WP-API/issues/283
1799
+ [gh-284]: https://github.com/WP-API/WP-API/issues/284
1800
+ [gh-285]: https://github.com/WP-API/WP-API/issues/285
1801
+ [gh-286]: https://github.com/WP-API/WP-API/issues/286
1802
+ [gh-288]: https://github.com/WP-API/WP-API/issues/288
1803
+ [gh-289]: https://github.com/WP-API/WP-API/issues/289
1804
+ [gh-290]: https://github.com/WP-API/WP-API/issues/290
1805
+ [gh-296]: https://github.com/WP-API/WP-API/issues/296
1806
+ [gh-298]: https://github.com/WP-API/WP-API/issues/298
1807
+ [gh-303]: https://github.com/WP-API/WP-API/issues/303
1808
+ [gh-304]: https://github.com/WP-API/WP-API/issues/304
1809
+ [gh-313]: https://github.com/WP-API/WP-API/issues/313
1810
+
1811
+ ## 1.0
1812
+
1813
+ - Add user endpoints.
1814
+
1815
+ Creating, reading, updating and deleting users and their data is now possible
1816
+ by using the `/users` endpoints. `/users/me` can be used to determine the
1817
+ current user, and returns a 401 status for non-logged in users.
1818
+
1819
+ Note that the format of post authors has changed, as it is now an embedded
1820
+ User entity. This should not break backwards compatibility.
1821
+
1822
+ Custom post types gain this ability automatically.
1823
+
1824
+ (props @tobych, @rmccue, [#20][gh-20], [#146][gh-146])
1825
+
1826
+ - Add post meta endpoints.
1827
+
1828
+ Creating, reading, updating and deleting post meta is now possible by using
1829
+ the `/posts/<id>/meta` endpoints. Post meta is now correctly embedded into
1830
+ Post entities.
1831
+
1832
+ Meta can be updated via the Post entity (e.g. `PUT` to `/posts/<id>`) or via
1833
+ the entity itself at `/posts/<id>/meta/<mid>`. Meta deletion must be done via
1834
+ a `DELETE` request to the latter.
1835
+
1836
+ Only non-protected and non-serialized meta can be accessed or manipulated via
1837
+ the API. This is not predicted to change in the future; clients wishing to
1838
+ access this data should consider alternative approaches.
1839
+
1840
+ Custom post types do not currently gain this ability automatically.
1841
+
1842
+ (props @attitude, @alisspers, @rachelbaker, @rmccue, @tlovett1, @tobych,
1843
+ @zedejose, [#68][gh-68], [#168][gh-168], [#189][gh-189], [#207][gh-207])
1844
+
1845
+ - Add endpoint for deleting a single comment.
1846
+
1847
+ Clients can now send a `DELETE` request to comment routes to delete
1848
+ the comment.
1849
+
1850
+ Custom post types supporting comments will gain this ability automatically.
1851
+
1852
+ (props @tlovett1, @rmccue, [#178][gh-178], [#191][gh-191])
1853
+
1854
+ - Add endpoint for post revisions.
1855
+
1856
+ Post revisions are now available at `/posts/<id>/revisions`, and are linked in
1857
+ the `meta.links.version-history` key of post entities.
1858
+
1859
+ Custom post types supporting revisions will gain this ability automatically.
1860
+
1861
+ (props @tlovett1, [#193][gh-193])
1862
+
1863
+ - Respond to requests without depending on pretty permalink settings.
1864
+
1865
+ For sites without pretty permalinks enabled, the API is now available from
1866
+ `?json_route=/`. Clients should check for this via the autodiscovery methods
1867
+ (Link header or RSD).
1868
+
1869
+ (props @rmccue, [#69][gh-69], [#138][gh-138])
1870
+
1871
+ - Add register post type argument.
1872
+
1873
+ Post types can now indicate their availability via the API using the
1874
+ `show_in_json` argument passed to `register_post_type`. This value defaults to
1875
+ the `publicly_queryable` argument (which itself defaults to the
1876
+ `public` argument).
1877
+
1878
+ (props @iandunn, @rmccue, [#145][gh-145])
1879
+
1880
+ - Remove basic authentication handler.
1881
+
1882
+ **This breaks backwards compatibility** for clients using Basic
1883
+ authentication. Clients are encouraged to switch to using [OAuth
1884
+ authentication][OAuth1]. The [Basic Authentication plugin][Basic-Auth] can be
1885
+ installed for backwards compatibility and local development, however should
1886
+ not be used in production.
1887
+
1888
+ (props @rmccue, [#37][gh-37], [#152][gh-152])
1889
+
1890
+ - Require nonces for cookie-based authentication.
1891
+
1892
+ **This breaks backwards compatibility** and requires any clients using cookie
1893
+ authentication to also send a nonce with the request. The built-in Javascript
1894
+ API automatically handles this.
1895
+
1896
+ (props @rmccue, [#177][gh-177], [#180][gh-180])
1897
+
1898
+ - Clean up deprecated methods/functions.
1899
+
1900
+ Functions and methods previously deprecated in 0.8/0.9 have now been removed.
1901
+ Future deprecations will take place in the same manner as WordPress core.
1902
+
1903
+ **This breaks backwards compatibility**, however these were marked as
1904
+ deprecated in previous releases.
1905
+
1906
+ (props @rmccue, [#187][gh-187])
1907
+
1908
+ - Only expose meta on 'edit' context as a temporary workaround.
1909
+
1910
+ Privacy concerns around exposing meta to all users necessitate this change.
1911
+
1912
+ **This breaks backwards compatibility** as post meta data is no longer
1913
+ available to all users. Clients wishing to access this data should
1914
+ authenticate and use the `edit` context.
1915
+
1916
+ (props @iandunn, @rmccue, [#135][gh-135])
1917
+
1918
+ - Add `json_ensure_response` function to ensure either a
1919
+ `WP_JSON_ResponseInterface` or a `WP_Error` object is returned.
1920
+
1921
+ When extending the API, the `json_ensure_response` function can be used to
1922
+ ensure that any raw data returned is wrapped with a `WP_JSON_Response` object.
1923
+ This allows using `get_status`/`get_data` easily, however `WP_Error` must
1924
+ still be checked via `is_wp_error`.
1925
+
1926
+ (props @rmccue, [#151][gh-151], [#154][gh-154])
1927
+
1928
+ - Use version option to check on init if rewrite rules should be flushed.
1929
+
1930
+ Rewrite rules on multisite are now flushed via an init hook, rather than
1931
+ switching to each site on activation.
1932
+
1933
+ (props @rachelbaker, [#149][gh-149])
1934
+
1935
+ - Fix typo in schema docs
1936
+
1937
+ (props @codebykat, [#132][gh-132])
1938
+
1939
+ - Add check for valid JSON data before using to avoid parameter overwrite.
1940
+
1941
+ When passing data to an endpoint that accepts JSON data, the data will now be
1942
+ validated before passing to the endpoint.
1943
+
1944
+ (props @rachelbaker, @rmccue, [#133][gh-133])
1945
+
1946
+ - Add authentication property to site index.
1947
+
1948
+ (props @rmccue, [#131][gh-131])
1949
+
1950
+ - Move the test helper to a subdirectory.
1951
+
1952
+ The plugin will now no longer prompt for updates due to the helper.
1953
+
1954
+ (props @rmccue, [#127][gh-127])
1955
+
1956
+ - Include post ID with `json_prepare_meta` filter.
1957
+
1958
+ (props @rmccue, [#137][gh-137])
1959
+
1960
+ - Corrected parameter names in x-form examples in docs.
1961
+
1962
+ (props @rachelbaker, [#134][gh-134])
1963
+
1964
+ - Pass `WP_JSON_Server` instance to `json_serve_request`.
1965
+
1966
+ (props @alisspers, @rmccue, [#61][gh-61], [#139][gh-139])
1967
+
1968
+ - Don't use deprecated function in `WP_JSON_Posts::edit_post()`
1969
+
1970
+ (props @rachelbaker, [#150][gh-150])
1971
+
1972
+ - Pass post ID to `json_insert_post` action during both insert and update.
1973
+
1974
+ (props @cmmarslender, [#148][gh-148])
1975
+
1976
+ - Add descriptions to taxonomy term data.
1977
+
1978
+ (props @pushred, [#111][gh-111])
1979
+
1980
+ - Ensure we handle raw data passed to the API.
1981
+
1982
+ (props @tlovett1, @rmccue, [#91][gh-91], [#155][gh-155])
1983
+
1984
+ - Remove unused `prepare_author` method from `WP_JSON_Posts` class.
1985
+
1986
+ (props @rachelbaker, [#165][gh-165])
1987
+
1988
+ - Add multiple post type support to get_posts method.
1989
+
1990
+ (props @rmccue, [#142][gh-142], [#163][gh-163])
1991
+
1992
+ - Return `WP_Error` in `WP_JSON_Posts::get_comment` for invalid comments.
1993
+
1994
+ (props @tlovett1, [#166][gh-166], [#171][gh-171])
1995
+
1996
+ - Update getting started documentation.
1997
+
1998
+ (props @rmccue, [#176][gh-176])
1999
+
2000
+ - Improve and clarify "array" input syntax documentation.
2001
+
2002
+ (props @rmccue, [#140][gh-140], [#175][gh-175])
2003
+
2004
+ - Update post routes documentation.
2005
+
2006
+ (props @rmccue, [#172][gh-172], [#174][gh-174])
2007
+
2008
+ - Add documentation for user endpoints.
2009
+
2010
+ (props @rachelbaker, @rmccue, [#158][gh-158])
2011
+
2012
+ - Add permalink settings step to Quick Setup instructions.
2013
+
2014
+ (props @kadamwhite, [#183][gh-183])
2015
+
2016
+ - Update taxonomy collection to return indexed array.
2017
+
2018
+ (props @mattheu, [#184][gh-184])
2019
+
2020
+ - Remove placeholder endpoints.
2021
+
2022
+ (props @rmccue, [#161][gh-161], [#192][gh-192])
2023
+
2024
+ - Fix issues with embedded attachments.
2025
+
2026
+ Checks that the post supports attachment data before adding it, and ensures we
2027
+ don't embed entities many layers deep.
2028
+
2029
+ (props @rmccue, [#194][gh-194])
2030
+
2031
+ - Change post parent preparation context to embed.
2032
+
2033
+ (props @rmccue, [#195][gh-195])
2034
+
2035
+ - Change server meta links to reference the WP-API organization GitHub repo.
2036
+
2037
+ (props @rachelbaker, [#208][gh-208])
2038
+
2039
+ - Fix plugin tests
2040
+
2041
+ (props @rmccue, [#215][gh-215])
2042
+
2043
+ - Check for errors with invalid dates and remove duplicate date parsing
2044
+ methods.
2045
+
2046
+ (props @rachelbaker, @rmccue, [#216][gh-216], [#219][gh-219])
2047
+
2048
+ [View all changes](https://github.com/rmccue/WP-API/compare/0.9...1.0)
2049
+
2050
+ [OAuth1]: https://github.com/WP-API/OAuth1
2051
+ [Basic-Auth]: https://github.com/WP-API/Basic-Auth
2052
+ [gh-20]: https://github.com/WP-API/WP-API/issues/20
2053
+ [gh-37]: https://github.com/WP-API/WP-API/issues/37
2054
+ [gh-61]: https://github.com/WP-API/WP-API/issues/61
2055
+ [gh-68]: https://github.com/WP-API/WP-API/issues/68
2056
+ [gh-69]: https://github.com/WP-API/WP-API/issues/69
2057
+ [gh-91]: https://github.com/WP-API/WP-API/issues/91
2058
+ [gh-111]: https://github.com/WP-API/WP-API/issues/111
2059
+ [gh-127]: https://github.com/WP-API/WP-API/issues/127
2060
+ [gh-131]: https://github.com/WP-API/WP-API/issues/131
2061
+ [gh-132]: https://github.com/WP-API/WP-API/issues/132
2062
+ [gh-133]: https://github.com/WP-API/WP-API/issues/133
2063
+ [gh-134]: https://github.com/WP-API/WP-API/issues/134
2064
+ [gh-135]: https://github.com/WP-API/WP-API/issues/135
2065
+ [gh-137]: https://github.com/WP-API/WP-API/issues/137
2066
+ [gh-138]: https://github.com/WP-API/WP-API/issues/138
2067
+ [gh-139]: https://github.com/WP-API/WP-API/issues/139
2068
+ [gh-140]: https://github.com/WP-API/WP-API/issues/140
2069
+ [gh-142]: https://github.com/WP-API/WP-API/issues/142
2070
+ [gh-145]: https://github.com/WP-API/WP-API/issues/145
2071
+ [gh-146]: https://github.com/WP-API/WP-API/issues/146
2072
+ [gh-148]: https://github.com/WP-API/WP-API/issues/148
2073
+ [gh-149]: https://github.com/WP-API/WP-API/issues/149
2074
+ [gh-150]: https://github.com/WP-API/WP-API/issues/150
2075
+ [gh-151]: https://github.com/WP-API/WP-API/issues/151
2076
+ [gh-152]: https://github.com/WP-API/WP-API/issues/152
2077
+ [gh-154]: https://github.com/WP-API/WP-API/issues/154
2078
+ [gh-155]: https://github.com/WP-API/WP-API/issues/155
2079
+ [gh-158]: https://github.com/WP-API/WP-API/issues/158
2080
+ [gh-161]: https://github.com/WP-API/WP-API/issues/161
2081
+ [gh-163]: https://github.com/WP-API/WP-API/issues/163
2082
+ [gh-165]: https://github.com/WP-API/WP-API/issues/165
2083
+ [gh-166]: https://github.com/WP-API/WP-API/issues/166
2084
+ [gh-168]: https://github.com/WP-API/WP-API/issues/168
2085
+ [gh-171]: https://github.com/WP-API/WP-API/issues/171
2086
+ [gh-172]: https://github.com/WP-API/WP-API/issues/172
2087
+ [gh-174]: https://github.com/WP-API/WP-API/issues/174
2088
+ [gh-175]: https://github.com/WP-API/WP-API/issues/175
2089
+ [gh-176]: https://github.com/WP-API/WP-API/issues/176
2090
+ [gh-177]: https://github.com/WP-API/WP-API/issues/177
2091
+ [gh-178]: https://github.com/WP-API/WP-API/issues/178
2092
+ [gh-180]: https://github.com/WP-API/WP-API/issues/180
2093
+ [gh-183]: https://github.com/WP-API/WP-API/issues/183
2094
+ [gh-184]: https://github.com/WP-API/WP-API/issues/184
2095
+ [gh-187]: https://github.com/WP-API/WP-API/issues/187
2096
+ [gh-189]: https://github.com/WP-API/WP-API/issues/189
2097
+ [gh-191]: https://github.com/WP-API/WP-API/issues/191
2098
+ [gh-192]: https://github.com/WP-API/WP-API/issues/192
2099
+ [gh-193]: https://github.com/WP-API/WP-API/issues/193
2100
+ [gh-194]: https://github.com/WP-API/WP-API/issues/194
2101
+ [gh-195]: https://github.com/WP-API/WP-API/issues/195
2102
+ [gh-207]: https://github.com/WP-API/WP-API/issues/207
2103
+ [gh-208]: https://github.com/WP-API/WP-API/issues/208
2104
+ [gh-215]: https://github.com/WP-API/WP-API/issues/215
2105
+ [gh-216]: https://github.com/WP-API/WP-API/issues/216
2106
+ [gh-219]: https://github.com/WP-API/WP-API/issues/219
2107
+
2108
+ ## 0.9
2109
+
2110
+ - Move from `wp-json.php/` to `wp-json/`
2111
+
2112
+ **This breaks backwards compatibility** and requires any clients to now use
2113
+ `wp-json/`, or preferably the new RSD/Link headers.
2114
+
2115
+ (props @rmccue, @matrixik, [#46][gh-46], [#96][gh-96], [#106][gh-106])
2116
+
2117
+ - Move filter registration out of CPT constructor. CPT subclasses now require
2118
+ you to call `$myobject->register_filters()`, in order to move global state out
2119
+ of the constructor.
2120
+
2121
+ **This breaks backwards compatibility** and requires any subclassing to now
2122
+ call `$myobject->register_filters()`
2123
+
2124
+ (props @rmccue, @thenbrent, [#42][gh-42], [#126][gh-126])
2125
+
2126
+ - Introduce Response/ResponseInterface
2127
+
2128
+ Endpoints that need to set headers or response codes should now return a
2129
+ `WP_JSON_Response` rather than using the server methods.
2130
+ `WP_JSON_ResponseInterface` may also be used for more flexible use of the
2131
+ response methods.
2132
+
2133
+ **Deprecation warning:** Calling `WP_JSON_Server::header`,
2134
+ `WP_JSON_Server::link_header` and `WP_JSON_Server::query_navigation_headers`
2135
+ is now deprecated. This will be removed in 1.0.
2136
+
2137
+ (props @rmccue, [#33][gh-33])
2138
+
2139
+ - Change all semiCamelCase names to underscore_case.
2140
+
2141
+ **Deprecation warning**: Any calls to semiCamelCase methods require any
2142
+ subclassing to update method references. This will be removed in 1.0.
2143
+
2144
+ (props @osiux, [#36][gh-36], [#82][gh-82])
2145
+
2146
+ - Add multisite compatibility. If the plugin is network activated, the plugin is
2147
+ now activated once-per-site, so `wp-json/` is always site-local.
2148
+
2149
+ (props @rachelbaker, [#48][gh-48], [#49][gh-49])
2150
+
2151
+ - Add RSD and Link headers for discovery
2152
+
2153
+ (props @rmccue, [#40][gh-40])
2154
+
2155
+ - WP_JSON_Posts->prepare_author() now verifies the `$user` object is set.
2156
+
2157
+ (props @rachelbaker, [#51][gh-51], [#54][gh-54])
2158
+
2159
+ - Added unit testing framework. Currently only a smaller number of tests, but we
2160
+ plan to increase this significantly as soon as possible.
2161
+
2162
+ (props @tierra, @osiux, [#65][gh-65], [#76][gh-76], [#84][gh-84])
2163
+
2164
+ - Link collection filtering docs to URL formatting guide.
2165
+
2166
+ (props @kadamwhite, [#74][gh-74])
2167
+
2168
+ - Remove hardcoded `/pages` references from `WP_JSON_Pages`
2169
+
2170
+ (props @rmccue, @thenbrent, [#28][gh-28], [#78][gh-78])
2171
+
2172
+ - Fix compatibility with `DateTime::createFromFormat` on PHP 5.2
2173
+
2174
+ (props @osiux, [#52][gh-52], [#79][gh-79])
2175
+
2176
+ - Document that `WP_JSON_CustomPostType::__construct()` requires a param of type
2177
+ `WP_JSON_ResponseHandler`.
2178
+
2179
+ (props @tlovett1, [#88][gh-88])
2180
+
2181
+ - Add timezone parameter to WP_JSON_DateTime::createFromFormat()
2182
+
2183
+ (props @royboy789, @rachelbaker, [#85][gh-85], [#87][gh-87])
2184
+
2185
+ - Remove IXR references. `IXR_Error` is no longer accepted as a return value.
2186
+
2187
+ **This breaks backwards compatibility** and requires anyone returning
2188
+ `IXR_Error` objects to now return `WP_Error` or `WP_JSON_ResponseInterface`
2189
+ objects.
2190
+
2191
+ (props @rmccue, [#50][gh-50], [#77][gh-77])
2192
+
2193
+ - Fix bugs with attaching featured images to posts:
2194
+ - `WP_JSON_Media::attachThumbnail()` should do nothing if `$update` is false
2195
+ without a post ID
2196
+ - The post ID must be fetched from the `$post` array.
2197
+
2198
+ (props @Webbgaraget, [#55][gh-55])
2199
+
2200
+ - Don't declare `jsonSerialize` on ResponseInterface
2201
+
2202
+ (props @rmccue, [#97][gh-97])
2203
+
2204
+ - Allow JSON post creation/update for `WP_JSON_CustomPostType`
2205
+
2206
+ (props @tlovett1, [#90][gh-90], [#108][gh-108])
2207
+
2208
+ - Return null if post doesn't have an excerpt
2209
+
2210
+ (props @rachelbacker, [#72][gh-72])
2211
+
2212
+ - Fix link to issue tracker in README
2213
+
2214
+ (props @rmccue, @tobych, [#125][gh-125])
2215
+
2216
+ [View all changes](https://github.com/rmccue/WP-API/compare/0.8...0.9)
2217
+
2218
+ [gh-28]: https://github.com/WP-API/WP-API/issues/28
2219
+ [gh-33]: https://github.com/WP-API/WP-API/issues/33
2220
+ [gh-36]: https://github.com/WP-API/WP-API/issues/36
2221
+ [gh-40]: https://github.com/WP-API/WP-API/issues/40
2222
+ [gh-42]: https://github.com/WP-API/WP-API/issues/42
2223
+ [gh-46]: https://github.com/WP-API/WP-API/issues/46
2224
+ [gh-48]: https://github.com/WP-API/WP-API/issues/48
2225
+ [gh-49]: https://github.com/WP-API/WP-API/issues/49
2226
+ [gh-50]: https://github.com/WP-API/WP-API/issues/50
2227
+ [gh-51]: https://github.com/WP-API/WP-API/issues/51
2228
+ [gh-52]: https://github.com/WP-API/WP-API/issues/52
2229
+ [gh-54]: https://github.com/WP-API/WP-API/issues/54
2230
+ [gh-55]: https://github.com/WP-API/WP-API/issues/55
2231
+ [gh-65]: https://github.com/WP-API/WP-API/issues/65
2232
+ [gh-72]: https://github.com/WP-API/WP-API/issues/72
2233
+ [gh-74]: https://github.com/WP-API/WP-API/issues/74
2234
+ [gh-76]: https://github.com/WP-API/WP-API/issues/76
2235
+ [gh-77]: https://github.com/WP-API/WP-API/issues/77
2236
+ [gh-78]: https://github.com/WP-API/WP-API/issues/78
2237
+ [gh-79]: https://github.com/WP-API/WP-API/issues/79
2238
+ [gh-82]: https://github.com/WP-API/WP-API/issues/82
2239
+ [gh-84]: https://github.com/WP-API/WP-API/issues/84
2240
+ [gh-85]: https://github.com/WP-API/WP-API/issues/85
2241
+ [gh-87]: https://github.com/WP-API/WP-API/issues/87
2242
+ [gh-88]: https://github.com/WP-API/WP-API/issues/88
2243
+ [gh-90]: https://github.com/WP-API/WP-API/issues/90
2244
+ [gh-96]: https://github.com/WP-API/WP-API/issues/96
2245
+ [gh-97]: https://github.com/WP-API/WP-API/issues/97
2246
+ [gh-106]: https://github.com/WP-API/WP-API/issues/106
2247
+ [gh-108]: https://github.com/WP-API/WP-API/issues/108
2248
+ [gh-125]: https://github.com/WP-API/WP-API/issues/125
2249
+ [gh-126]: https://github.com/WP-API/WP-API/issues/126
2250
+
2251
+ ## 0.8
2252
+ - Add compatibility layer for JsonSerializable. You can now return arbitrary
2253
+ objects from endpoints and use the `jsonSerialize()` method to return the data
2254
+ to serialize instead of just using the properties of the object.
2255
+
2256
+ (props @rmccue, [#24][gh-24])
2257
+
2258
+ - Fix page parent links to use `/pages`
2259
+
2260
+ (props @thenbrent, [#27][gh-27])
2261
+
2262
+ - Remove redundant `WP_JSON_Pages::type_archive_link()` function
2263
+
2264
+ (props @thenbrent, [#29][gh-29])
2265
+
2266
+ - Removed unneeded executable bit on all files
2267
+
2268
+ (props @tierra, [#31][gh-31])
2269
+
2270
+ - Don't include the `featured_image` property for post types that don't
2271
+ support thumbnails
2272
+
2273
+ (props @phh, [#43][gh-43])
2274
+
2275
+ - Use `wp_json_server_before_serve` instead of `plugins_loaded` in the Extending
2276
+ documentation for plugins
2277
+
2278
+ (props @phh, [#43][gh-43])
2279
+
2280
+ - Parse the avatar URL from the `get_avatar()` function in core, allowing custom
2281
+ avatar implementations
2282
+
2283
+ (props @rachelbaker, [#47][gh-47], [#35][gh-35])
2284
+
2285
+ - Ensure that the author is set if passed
2286
+
2287
+ (props @kuchenundkakao, [#44][gh-44])
2288
+
2289
+ - Clarify the usage of `WP_JSON_CustomPostType` in plugins
2290
+
2291
+ (props @rmccue, [#45][gh-45])
2292
+
2293
+ - Ensure JSON disabled error messages are translated
2294
+
2295
+ (props @rmccue, [#38][gh-38])
2296
+
2297
+ - Remove extra "Link: " from link headers
2298
+
2299
+ (props @jmusal, [#56][gh-56], [#30][gh-30])
2300
+
2301
+ - Remove redundant `get_avatar` method in `WP_JSON_Posts`
2302
+
2303
+ (props @rachelbaker, [#35][gh-35])
2304
+
2305
+ - Rename `WP_JSON_Server::get_avatar()` to `WP_JSON_Server::get_avatar_url()`
2306
+
2307
+ (props @rachelbaker, [#35][gh-35])
2308
+
2309
+ [View all changes](https://github.com/rmccue/WP-API/compare/0.7...0.8)
2310
+
2311
+ [gh-24]: https://github.com/WP-API/WP-API/issues/24
2312
+ [gh-27]: https://github.com/WP-API/WP-API/issues/27
2313
+ [gh-29]: https://github.com/WP-API/WP-API/issues/29
2314
+ [gh-30]: https://github.com/WP-API/WP-API/issues/30
2315
+ [gh-31]: https://github.com/WP-API/WP-API/issues/31
2316
+ [gh-35]: https://github.com/WP-API/WP-API/issues/35
2317
+ [gh-38]: https://github.com/WP-API/WP-API/issues/38
2318
+ [gh-43]: https://github.com/WP-API/WP-API/issues/43
2319
+ [gh-43]: https://github.com/WP-API/WP-API/issues/43
2320
+ [gh-44]: https://github.com/WP-API/WP-API/issues/44
2321
+ [gh-45]: https://github.com/WP-API/WP-API/issues/45
2322
+ [gh-47]: https://github.com/WP-API/WP-API/issues/47
2323
+ [gh-56]: https://github.com/WP-API/WP-API/issues/56
2324
+
2325
+ ## 0.7
2326
+ - The response handler object is now passed into the endpoint objects via the
2327
+ constructor, allowing you to avoid excess global state where possible. It's
2328
+ recommended to use this where possible rather than the global object.
2329
+
2330
+ (props @rmccue, [#2][gh-2])
2331
+
2332
+ - Fix undefined variables and indices
2333
+ (props @pippinsplugins, [#5][gh-5])
2334
+
2335
+ - Correct call to deactivation hook
2336
+ (props @ericpedia, [#9][gh-9])
2337
+
2338
+ - Check metadata access correctly rather than always hiding for users without
2339
+ the `edit_post_meta` capability
2340
+ (props @kokarn, [#10][gh-10])
2341
+
2342
+ - Return all term metadata, rather than just the last one
2343
+ (props @afurculita, [#13][gh-13])
2344
+
2345
+ - Access post metadata from cache where possible - Note, this is a backwards
2346
+ compatibility break, as the format of the metadata has changed. This may
2347
+ change again in the near future, so don't rely on it until 1.0.
2348
+ (props @afurculita, [#14][gh-14])
2349
+
2350
+ - Add term_link to prepare_term
2351
+ (props @afurculita, [#15][gh-15])
2352
+
2353
+ - Fix hardcoded `/pages` references in `WP_JSON_CustomPostType`
2354
+ (props @thenbrent, [#26][gh-26])
2355
+
2356
+ - Sanitize headers for newlines
2357
+ (props @kokarn, [#7][gh-7])
2358
+
2359
+ - Register rewrite rules during plugin activation
2360
+ (props @pippinsplugins, [#17][gh-17])
2361
+
2362
+ [View all changes](https://github.com/rmccue/WP-API/compare/0.6...0.7)
2363
+
2364
+ [gh-2]: https://github.com/WP-API/WP-API/issues/2
2365
+ [gh-5]: https://github.com/WP-API/WP-API/issues/5
2366
+ [gh-7]: https://github.com/WP-API/WP-API/issues/7
2367
+ [gh-9]: https://github.com/WP-API/WP-API/issues/9
2368
+ [gh-10]: https://github.com/WP-API/WP-API/issues/10
2369
+ [gh-13]: https://github.com/WP-API/WP-API/issues/13
2370
+ [gh-14]: https://github.com/WP-API/WP-API/issues/14
2371
+ [gh-15]: https://github.com/WP-API/WP-API/issues/15
2372
+ [gh-17]: https://github.com/WP-API/WP-API/issues/17
2373
+ [gh-26]: https://github.com/WP-API/WP-API/issues/26
2374
+
2375
+ ## 0.6
2376
+ - Huge documentation update - Guides on getting started and extending the API
2377
+ are [now available for your perusal][docs]
2378
+ - Add generic CPT class - Plugins are now encouraged to extend
2379
+ `WP_JSON_CustomPostType` and get free hooking for common actions. This
2380
+ removes most of the boilerplate that you needed to write for new CPT-based
2381
+ routes and endpoints ([#380][])
2382
+ - Use defined filter priorities for endpoint registration - It's now easier to
2383
+ inject your own endpoints at a defined point
2384
+ - Update the schema - Now includes documentation on the Media entity, plus more
2385
+ ([#264][])
2386
+ - Add better taxonomy support - You can now query for taxonomies and terms
2387
+ directly. The routes here might seem strange
2388
+ (`/posts/types/post/taxonomies/category` for example), but the intention is
2389
+ to [future-proof them](http://make.wordpress.org/core/2013/07/28/potential-roadmap-for-taxonomy-meta-and-post-relationships/)
2390
+ as much as possible([#275][])
2391
+ - Ensure the JSON URL is relative to the home URL ([#375][])
2392
+ - Check all date formats for If-Unmodified-Since ([#378][])
2393
+ - Register the correct URL for the JS library ([#376][])
2394
+ - Correct the usage of meta links ([#379][])
2395
+ - Add filters for post type and post status data ([#380][])
2396
+ - Separate parent post and parent comment relation ([#330][]()
2397
+
2398
+ [View all changes](https://github.com/rmccue/WP-API/compare/0.5...0.6)
2399
+
2400
+ [docs]: https://github.com/rmccue/WP-API/tree/master/docs
2401
+
2402
+ [#264]: https://gsoc.trac.wordpress.org/ticket/264
2403
+ [#275]: https://gsoc.trac.wordpress.org/ticket/275
2404
+ [#330]: https://gsoc.trac.wordpress.org/ticket/330
2405
+ [#375]: https://gsoc.trac.wordpress.org/ticket/375
2406
+ [#376]: https://gsoc.trac.wordpress.org/ticket/376
2407
+ [#378]: https://gsoc.trac.wordpress.org/ticket/378
2408
+ [#379]: https://gsoc.trac.wordpress.org/ticket/379
2409
+ [#380]: https://gsoc.trac.wordpress.org/ticket/380
2410
+
2411
+
2412
+ ## 0.5
2413
+ - Add support for media - This has been a long time coming, and it's finally at
2414
+ a point where I'm happy to push it out. Good luck. ([#272][])
2415
+ - Separate the post-related endpoints - Post-related endpoints are now located
2416
+ in the `WP_JSON_Posts` class. When implementing custom post type support,
2417
+ it's recommended to subclass this.
2418
+
2419
+ The various types are now also only registered via hooks, rather than
2420
+ directly in the server class, which should make it easier to override them
2421
+ as well ([#348][])
2422
+ - Add page support - This is a good base if you're looking to create your own
2423
+ custom post type support ([#271][])
2424
+ - Switch from fields to context - Rather than passing in a list of fields that
2425
+ you want, you can now pass in a context (usually `view` or `edit`)
2426
+ ([#328][]).
2427
+ - Always send headers via the server handler - Endpoints are now completely
2428
+ separate from the request, so the server class can now be used for
2429
+ non-HTTP/JSON handlers if needed ([#293][])
2430
+ - Use better error codes for disabled features ([#338][])
2431
+ - Send `X-WP-Total` and `X-WP-TotalPages` headers for information on
2432
+ post/pagination counts ([#266][])
2433
+
2434
+ [View all changes](https://github.com/rmccue/WP-API/compare/0.4...0.5)
2435
+
2436
+ [#266]: https://gsoc.trac.wordpress.org/ticket/266
2437
+ [#271]: https://gsoc.trac.wordpress.org/ticket/271
2438
+ [#272]: https://gsoc.trac.wordpress.org/ticket/272
2439
+ [#293]: https://gsoc.trac.wordpress.org/ticket/293
2440
+ [#328]: https://gsoc.trac.wordpress.org/ticket/328
2441
+ [#338]: https://gsoc.trac.wordpress.org/ticket/338
2442
+ [#348]: https://gsoc.trac.wordpress.org/ticket/348
2443
+
2444
+
2445
+ ## 0.4
2446
+ - Add Backbone-based models and collections - These are available to your code
2447
+ by declaring a dependency on `wp-api` ([#270][])
2448
+ - Check `json_route` before using it ([#336][])
2449
+ - Conditionally load classes ([#337][])
2450
+ - Add additional test helper plugin - Provides code coverage as needed to the
2451
+ API client tests. Currently unused. ([#269][])
2452
+ - Move `json_url()` and `get_json_url()` to `plugin.php` - This allows using
2453
+ both outside of the API itself ([#343][])
2454
+ - `getPost(0)` now returns an error rather than the latest post ([#344][])
2455
+
2456
+ [View all changes](https://github.com/rmccue/WP-API/compare/0.3...0.4)
2457
+
2458
+ [#269]: https://gsoc.trac.wordpress.org/ticket/269
2459
+ [#270]: https://gsoc.trac.wordpress.org/ticket/270
2460
+ [#336]: https://gsoc.trac.wordpress.org/ticket/336
2461
+ [#337]: https://gsoc.trac.wordpress.org/ticket/337
2462
+ [#343]: https://gsoc.trac.wordpress.org/ticket/343
2463
+ [#344]: https://gsoc.trac.wordpress.org/ticket/344
2464
+
2465
+ ## 0.3
2466
+ - Add initial comment endpoints to get comments for a post, and get a single
2467
+ comment ([#320][])
2468
+ - Return a Post entity when updating a post, rather than wrapping it with
2469
+ useless text ([#329][])
2470
+ - Allow filtering the output as well as input. You can now use the
2471
+ `json_dispatch_args` filter for input as well as the `json_serve_request`
2472
+ filter for output to serve up alternative formats (e.g. MsgPack, XML (if
2473
+ you're insane))
2474
+ - Include a `profile` link in the index, to indicate the JSON Schema that the
2475
+ API conforms to. In the future, this will be versioned.
2476
+
2477
+ [#320]: https://gsoc.trac.wordpress.org/ticket/320
2478
+ [#329]: https://gsoc.trac.wordpress.org/ticket/329
2479
+
2480
+ ## 0.2
2481
+ - Allow all public query vars to be passed to WP Query - Some private query vars
2482
+ can also be passed in, and all can if the user has `edit_posts`
2483
+ permissions ([#311][])
2484
+ - Pagination can now be handled by using the `page` argument without messing
2485
+ with WP Query syntax ([#266][])
2486
+ - The index now generates links for non-variable routes ([#268][])
2487
+ - Editing a post now supports the `If-Unmodified-Since` header. Pass this in to
2488
+ avoid conflicting edits ([#294][])
2489
+ - Post types and post statuses now have endpoints to access their data ([#268][])
2490
+
2491
+ [View all changes](https://github.com/rmccue/WP-API/compare/0.1.2...0.2)
2492
+
2493
+ [#268]: https://gsoc.trac.wordpress.org/ticket/268
2494
+ [#294]: https://gsoc.trac.wordpress.org/ticket/294
2495
+ [#266]: https://gsoc.trac.wordpress.org/ticket/266
2496
+ [#311]: https://gsoc.trac.wordpress.org/ticket/311
2497
+
2498
+ ## 0.1.2
2499
+ - Disable media handling to avoid fatal error ([#298][])
2500
+
2501
+ [#298]: http://gsoc.trac.wordpress.org/ticket/298
2502
+
2503
+ ## 0.1.1
2504
+ - No changes, process error
2505
+
2506
+ ## 0.1
2507
+ - Enable the code to be used via the plugin architecture (now uses rewrite rules
2508
+ if running in this mode)
2509
+ - Design documents are now functionally complete for the current codebase
2510
+ ([#264][])
2511
+ - Add basic writing support ([#265][])
2512
+ - Filter fields by default - Unfiltered results are available via their
2513
+ corresponding `*_raw` key, which is only available to users with
2514
+ `edit_posts` ([#290][])
2515
+ - Use correct timezones for manual offsets (GMT+10, e.g.) ([#279][])
2516
+ - Allow permanently deleting posts ([#292])
2517
+
2518
+ [View all changes](https://github.com/rmccue/WP-API/compare/b3a8d7656ffc58c734aad95e0839609011b26781...0.1.1)
2519
+
2520
+ [#264]: https://gsoc.trac.wordpress.org/ticket/264
2521
+ [#265]: https://gsoc.trac.wordpress.org/ticket/265
2522
+ [#279]: https://gsoc.trac.wordpress.org/ticket/279
2523
+ [#290]: https://gsoc.trac.wordpress.org/ticket/290
2524
+ [#292]: https://gsoc.trac.wordpress.org/ticket/292
2525
+
2526
+ ## 0.0.4
2527
+ - Hyperlinks now available in most constructs under the 'meta' key. At the
2528
+ moment, the only thing under this key is 'links', but more will come
2529
+ eventually. (Try browsing with a browser tool like JSONView; you should be
2530
+ able to view all content just by clicking the links.)
2531
+ - Accessing / now gives an index which briefly describes the API and gives
2532
+ links to more (also added the HIDDEN_ENDPOINT constant to hide from this).
2533
+ - Post collections now contain a summary of the post, with the full post
2534
+ available via the single post call. (prepare_post() has fields split into
2535
+ post and post-extended)
2536
+ - Post entities have dropped post_ prefixes, and custom_fields has changed to
2537
+ post_meta.
2538
+ - Now supports JSONP callback via the _jsonp argument. This can be disabled
2539
+ separately to the API itself, as it's only needed for
2540
+ cross-origin requests.
2541
+ - Internal: No longer extends the XMLRPC class. All relevant pieces have been
2542
+ copied over. Further work still needs to be done on this, but it's a start.
2543
+
2544
+ ## 0.0.3:
2545
+ - Now accepts JSON bodies if an endpoint is marked with ACCEPT_JSON
CONTRIBUTING.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing
2
+ Hi, and thanks for considering contributing! Before you do though, here's a few
3
+ notes on how best to contribute. Don't worry, I'll keep it short!
4
+
5
+ ## Best Practices
6
+
7
+ ### Commit Messages
8
+ Commit messages should follow the standard laid out in the git manual; that is,
9
+ a one-line summary ()
10
+
11
+ Short (50 chars or less) summary of changes
12
+
13
+ More detailed explanatory text, if necessary. Wrap it to about 72
14
+ characters or so. In some contexts, the first line is treated as the
15
+ subject of an email and the rest of the text as the body. The blank
16
+ line separating the summary from the body is critical (unless you omit
17
+ the body entirely); tools like rebase can get confused if you run the
18
+ two together.
19
+
20
+ Further paragraphs come after blank lines.
21
+
22
+ - Bullet points are okay, too
23
+
24
+ - Typically a hyphen or asterisk is used for the bullet, preceded by a
25
+ single space, with blank lines in between, but conventions vary here
26
+
27
+ ## Commit Process
28
+ Changes are proposed in the form of pull requests by you, the contributor! After
29
+ submitting your proposed changes, a member of the API team will review your
30
+ commits and mark them for merge by assigning it to themselves. Your pull request
31
+ will then be merged after final review by another member.
README.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # WP REST API v2.0 (WP-API)
2
+
3
+ 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
+
10
+ The **"develop"** branch is undergoing substantial changes and is **NOT COMPLETE OR STABLE**. [Read the in-progress documentation](http://v2.wp-api.org/) to introduce yourself to endpoints, internal patterns, and implementation details.
11
+
12
+ The **"master"** branch represents a **BETA** of our next version release.
13
+
14
+ The latest **stable** version is available from the [WordPress Plugin Directory](https://wordpress.org/plugins/json-rest-api/).
15
+
16
+ ## About
17
+
18
+ WordPress is moving towards becoming a fully-fledged application framework, and
19
+ we need new APIs. This project was born to create an easy-to-use,
20
+ easy-to-understand and well-tested framework for creating these APIs, plus
21
+ creating APIs for core.
22
+
23
+ This plugin provides an easy to use REST API, available via HTTP. Grab your
24
+ site's data in simple JSON format, including users, posts, taxonomies and more.
25
+ 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
33
+ API, users API, revisions API and many more. Chances are, if you can do it with
34
+ WordPress, WP API will let you do it.
35
+
36
+ WP API also includes an easy-to-use JavaScript API based on Backbone models,
37
+ allowing plugin and theme developers to get up and running without needing to
38
+ know anything about the details of getting connected.
39
+
40
+ Check out [our documentation][docs] for information on what's available in the
41
+ API and how to use it. We've also got documentation on extending the API with
42
+ extra data for plugin and theme developers!
43
+
44
+ There's no fixed timeline for integration into core at this time, but getting closer!
45
+
46
+
47
+ ## Installation
48
+
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
55
+ look at the [recent updates][] for the project.
56
+
57
+ ## Security
58
+
59
+ We take the security of the API extremely seriously. If you think you've found
60
+ a security issue with the API (whether information disclosure, privilege
61
+ escalation, or another issue), we'd appreciate responsible disclosure as soon as
62
+ possible.
63
+
64
+ To report a security issue, you can either email `security[at]wordpress.org`, or
65
+ [file an issue on HackerOne][hackerone]. We will attempt to give an initial
66
+ response to security issues within 48 hours at most, however keep in mind that
67
+ the team is distributed across various timezones, and delays may occur as we
68
+ discuss internally.
69
+
70
+ (Please note: For testing, you should install a copy of the project and
71
+ WordPress on your own server. **Do not test on servers you do not own.**)
72
+
73
+ ## License
74
+
75
+ [GPLv2+](http://www.gnu.org/licenses/gpl-2.0.html)
76
+
77
+ [docs]: http://v2.wp-api.org/
78
+ [GitHub]: https://github.com/WP-API/WP-API/issues
79
+ [recent updates]: https://make.wordpress.org/core/tag/json-api/
80
+ [hackerone]: https://hackerone.com/wp-api
compatibility-v1.php ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,1569 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ */
31
+ function rest_register_scripts() {
32
+ wp_register_script( 'wp-api', plugins_url( 'wp-api.js', __FILE__ ), array( 'jquery', 'backbone', 'underscore' ), '1.1', true );
33
+
34
+ $settings = array( 'root' => esc_url_raw( get_rest_url() ), 'nonce' => wp_create_nonce( 'wp_rest' ) );
35
+ wp_localize_script( 'wp-api', 'WP_API_Settings', $settings );
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();
170
+
171
+ $urls = array();
172
+ foreach ( $avatar_sizes as $size ) {
173
+ $urls[ $size ] = get_avatar_url( $email, array( 'size' => $size ) );
174
+ }
175
+
176
+ return $urls;
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.
297
+ preg_match( '/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches );
298
+
299
+ if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) {
300
+ return esc_url_raw( $matches[1] );
301
+ }
302
+
303
+ return '';
304
+ }
lib/endpoints/class-wp-rest-attachments-controller.php ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
4
+
5
+ /**
6
+ * Create a single attachment
7
+ *
8
+ * @param WP_REST_Request $request Full details about the request
9
+ * @return WP_Error|WP_REST_Response
10
+ */
11
+ public function create_item( $request ) {
12
+
13
+ // Permissions check - Note: "upload_files" cap is returned for an attachment by $post_type_obj->cap->create_posts
14
+ $post_type_obj = get_post_type_object( $this->post_type );
15
+ if ( ! current_user_can( $post_type_obj->cap->create_posts ) || ! current_user_can( $post_type_obj->cap->edit_posts ) ) {
16
+ return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to post on this site.' ), array( 'status' => 400 ) );
17
+ }
18
+
19
+ // If a user is trying to attach to a post make sure they have permissions. Bail early if post_id is not being passed
20
+ if ( ! empty( $request['post'] ) ) {
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
+
28
+ // Get the file via $_FILES or raw data
29
+ $files = $request->get_file_params();
30
+ $headers = $request->get_headers();
31
+ if ( ! empty( $files ) ) {
32
+ $file = $this->upload_from_file( $files, $headers );
33
+ } else {
34
+ $file = $this->upload_from_data( $request->get_body(), $headers );
35
+ }
36
+
37
+ if ( is_wp_error( $file ) ) {
38
+ return $file;
39
+ }
40
+
41
+ $name = basename( $file['file'] );
42
+ $name_parts = pathinfo( $name );
43
+ $name = trim( substr( $name, 0, -(1 + strlen( $name_parts['extension'] ) ) ) );
44
+
45
+ $url = $file['url'];
46
+ $type = $file['type'];
47
+ $file = $file['file'];
48
+ $title = $name;
49
+ $caption = '';
50
+
51
+ // use image exif/iptc data for title and caption defaults if possible
52
+ // @codingStandardsIgnoreStart
53
+ $image_meta = @wp_read_image_metadata( $file );
54
+ // @codingStandardsIgnoreEnd
55
+ if ( ! empty( $image_meta ) ) {
56
+ if ( empty( $request['title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
57
+ $title = $image_meta['title'];
58
+ }
59
+
60
+ if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) {
61
+ $caption = $image_meta['caption'];
62
+ }
63
+ }
64
+
65
+ $attachment = $this->prepare_item_for_database( $request );
66
+ $attachment->file = $file;
67
+ $attachment->post_mime_type = $type;
68
+ $attachment->guid = $url;
69
+ $id = wp_insert_post( $attachment, true );
70
+ if ( is_wp_error( $id ) ) {
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
+ }
89
+
90
+ /**
91
+ * Update a single post
92
+ *
93
+ * @param WP_REST_Request $request Full details about the request
94
+ * @return WP_Error|WP_REST_Response
95
+ */
96
+ public function update_item( $request ) {
97
+ $response = parent::update_item( $request );
98
+ if ( is_wp_error( $response ) ) {
99
+ return $response;
100
+ }
101
+
102
+ $response = rest_ensure_response( $response );
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
+ /**
119
+ * Prepare a single attachment for create or update
120
+ *
121
+ * @param WP_REST_Request $request Request object
122
+ * @return WP_Error|obj $prepared_attachment Post object
123
+ */
124
+ protected function prepare_item_for_database( $request ) {
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;
140
+ }
141
+
142
+ /**
143
+ * Prepare a single attachment output for response
144
+ *
145
+ * @param WP_Post $post Post object
146
+ * @param WP_REST_Request $request Request object
147
+ * @return array $response
148
+ */
149
+ public function prepare_item_for_response( $post, $request ) {
150
+ $response = parent::prepare_item_for_response( $post, $request );
151
+ $data = $response->get_data();
152
+
153
+ $data['alt_text'] = get_post_meta( $post->ID, '_wp_attachment_image_alt', true );
154
+ $data['caption'] = $post->post_excerpt;
155
+ $data['description'] = $post->post_content;
156
+ $data['media_type'] = wp_attachment_is_image( $post->ID ) ? 'image' : 'file';
157
+ $data['media_details'] = wp_get_attachment_metadata( $post->ID );
158
+ $data['post'] = ! empty( $post->post_parent ) ? (int) $post->post_parent : null;
159
+ $data['source_url'] = wp_get_attachment_url( $post->ID );
160
+
161
+ // Ensure empty details is an empty object
162
+ if ( empty( $data['media_details'] ) ) {
163
+ $data['media_details'] = new stdClass;
164
+ } elseif ( ! empty( $data['media_details']['sizes'] ) ) {
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;
173
+ }
174
+
175
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
176
+
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
+ /**
188
+ * Get the Attachment's schema, conforming to JSON Schema
189
+ *
190
+ * @return array
191
+ */
192
+ public function get_item_schema() {
193
+
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' ),
234
+ 'readonly' => true,
235
+ );
236
+ return $schema;
237
+ }
238
+
239
+ /**
240
+ * Handle an upload via raw POST data
241
+ *
242
+ * @param array $data Supplied file data
243
+ * @param array $headers HTTP headers from the request
244
+ * @return array|WP_Error Data from {@see wp_handle_sideload()}
245
+ */
246
+ protected function upload_from_data( $data, $headers ) {
247
+ if ( empty( $data ) ) {
248
+ return new WP_Error( 'rest_upload_no_data', __( 'No data supplied' ), array( 'status' => 400 ) );
249
+ }
250
+
251
+ if ( empty( $headers['content_type'] ) ) {
252
+ return new WP_Error( 'rest_upload_no_content_type', __( 'No Content-Type supplied' ), array( 'status' => 400 ) );
253
+ }
254
+
255
+ if ( empty( $headers['content_disposition'] ) ) {
256
+ return new WP_Error( 'rest_upload_no_content_disposition', __( 'No Content-Disposition supplied' ), array( 'status' => 400 ) );
257
+ }
258
+
259
+ // Get the filename
260
+ $filename = null;
261
+
262
+ foreach ( $headers['content_disposition'] as $part ) {
263
+ $part = trim( $part );
264
+
265
+ if ( strpos( $part, 'filename' ) !== 0 ) {
266
+ continue;
267
+ }
268
+
269
+ $filenameparts = explode( '=', $part );
270
+ $filename = trim( $filenameparts[1] );
271
+ }
272
+
273
+ if ( empty( $filename ) ) {
274
+ return new WP_Error( 'rest_upload_invalid_disposition', __( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as "filename=image.png" or similar.' ), array( 'status' => 400 ) );
275
+ }
276
+
277
+ if ( ! empty( $headers['content_md5'] ) ) {
278
+ $content_md5 = array_shift( $headers['content_md5'] );
279
+ $expected = trim( $content_md5 );
280
+ $actual = md5( $data );
281
+
282
+ if ( $expected !== $actual ) {
283
+ return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected' ), array( 'status' => 412 ) );
284
+ }
285
+ }
286
+
287
+ // Get the content-type
288
+ $type = array_shift( $headers['content_type'] );
289
+
290
+ // Save the file
291
+ $tmpfname = wp_tempnam( $filename );
292
+
293
+ $fp = fopen( $tmpfname, 'w+' );
294
+
295
+ if ( ! $fp ) {
296
+ return new WP_Error( 'rest_upload_file_error', __( 'Could not open file handle' ), array( 'status' => 500 ) );
297
+ }
298
+
299
+ fwrite( $fp, $data );
300
+ fclose( $fp );
301
+
302
+ // Now, sideload it in
303
+ $file_data = array(
304
+ 'error' => null,
305
+ 'tmp_name' => $tmpfname,
306
+ 'name' => $filename,
307
+ 'type' => $type,
308
+ );
309
+ $overrides = array(
310
+ 'test_form' => false,
311
+ );
312
+ $sideloaded = wp_handle_sideload( $file_data, $overrides );
313
+
314
+ if ( isset( $sideloaded['error'] ) ) {
315
+ // @codingStandardsIgnoreStart
316
+ @unlink( $tmpfname );
317
+ // @codingStandardsIgnoreEnd
318
+ return new WP_Error( 'rest_upload_sideload_error', $sideloaded['error'], array( 'status' => 500 ) );
319
+ }
320
+
321
+ return $sideloaded;
322
+ }
323
+
324
+ /**
325
+ * Handle an upload via multipart/form-data ($_FILES)
326
+ *
327
+ * @param array $files Data from $_FILES
328
+ * @param array $headers HTTP headers from the request
329
+ * @return array|WP_Error Data from {@see wp_handle_upload()}
330
+ */
331
+ protected function upload_from_file( $files, $headers ) {
332
+ if ( empty( $files ) ) {
333
+ return new WP_Error( 'rest_upload_no_data', __( 'No data supplied' ), array( 'status' => 400 ) );
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 ) );
342
+ }
343
+ }
344
+
345
+ // Pass off to WP to handle the actual upload
346
+ $overrides = array(
347
+ 'test_form' => false,
348
+ );
349
+ // Bypasses is_uploaded_file() when running unit tests
350
+ if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) {
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 ) );
358
+ }
359
+
360
+ return $file;
361
+ }
362
+
363
+ }
lib/endpoints/class-wp-rest-comments-controller.php ADDED
@@ -0,0 +1,1069 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Access comments
5
+ */
6
+ class WP_REST_Comments_Controller extends WP_REST_Controller {
7
+
8
+ /**
9
+ * Register the routes for the objects of the 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(
76
+ array(
77
+ 'methods' => WP_REST_Server::READABLE,
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,
127
+ 'callback' => array( $this, 'delete_item' ),
128
+ 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
129
+ 'args' => array(
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
+
141
+ /**
142
+ * Get a list of comments.
143
+ *
144
+ * @param WP_REST_Request $request Full details about the request.
145
+ * @return WP_Error|WP_REST_Response
146
+ */
147
+ public function get_items( $request ) {
148
+ $prepared_args = $this->prepare_items_query( $request );
149
+
150
+ $query = new WP_Comment_Query;
151
+ $query_result = $query->query( $prepared_args );
152
+
153
+ $comments = array();
154
+ foreach ( $query_result as $comment ) {
155
+ $post = get_post( $comment->comment_post_ID );
156
+ if ( ! $this->check_read_post_permission( $post ) || ! $this->check_read_permission( $comment ) ) {
157
+
158
+ continue;
159
+ }
160
+
161
+ $data = $this->prepare_item_for_response( $comment, $request );
162
+ $comments[] = $this->prepare_response_for_collection( $data );
163
+ }
164
+
165
+ $response = rest_ensure_response( $comments );
166
+ unset( $prepared_args['number'] );
167
+ unset( $prepared_args['offset'] );
168
+ $query = new WP_Comment_Query;
169
+ $prepared_args['count'] = true;
170
+ $total_comments = $query->query( $prepared_args );
171
+ $response->header( 'X-WP-Total', (int) $total_comments );
172
+ $max_pages = ceil( $total_comments / $request['per_page'] );
173
+ $response->header( 'X-WP-TotalPages', (int) $max_pages );
174
+
175
+ $base = add_query_arg( $request->get_query_params(), rest_url( '/wp/v2/comments' ) );
176
+ if ( $request['page'] > 1 ) {
177
+ $prev_page = $request['page'] - 1;
178
+ if ( $prev_page > $max_pages ) {
179
+ $prev_page = $max_pages;
180
+ }
181
+ $prev_link = add_query_arg( 'page', $prev_page, $base );
182
+ $response->link_header( 'prev', $prev_link );
183
+ }
184
+ if ( $max_pages > $request['page'] ) {
185
+ $next_page = $request['page'] + 1;
186
+ $next_link = add_query_arg( 'page', $next_page, $base );
187
+ $response->link_header( 'next', $next_link );
188
+ }
189
+
190
+ return $response;
191
+ }
192
+
193
+ /**
194
+ * Get a comment.
195
+ *
196
+ * @param WP_REST_Request $request Full details about the request.
197
+ * @return WP_Error|WP_REST_Response
198
+ */
199
+ public function get_item( $request ) {
200
+ $id = (int) $request['id'];
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 );
213
+ $response = rest_ensure_response( $data );
214
+
215
+ return $response;
216
+ }
217
+
218
+ /**
219
+ * Create a comment.
220
+ *
221
+ * @param WP_REST_Request $request Full details about the request.
222
+ * @return WP_Error|WP_REST_Response
223
+ */
224
+ public function create_item( $request ) {
225
+ if ( ! empty( $request['id'] ) ) {
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 );
244
+ if ( ! $comment_id ) {
245
+ return new WP_Error( 'rest_comment_failed_create', __( 'Creating comment failed.' ), array( 'status' => 500 ) );
246
+ }
247
+
248
+ if ( isset( $request['status'] ) ) {
249
+ $comment = get_comment( $comment_id );
250
+ $this->handle_status_param( $request['status'], $comment );
251
+ }
252
+
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;
263
+ }
264
+ $response->set_status( 201 );
265
+ $response->header( 'Location', rest_url( '/wp/v2/comments/' . $comment_id ) );
266
+
267
+ return $response;
268
+ }
269
+
270
+ /**
271
+ * Edit a comment
272
+ *
273
+ * @param WP_REST_Request $request Full details about the request.
274
+ * @return WP_Error|WP_REST_Response
275
+ */
276
+ public function update_item( $request ) {
277
+ $id = (int) $request['id'];
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 ) {
285
+ return new WP_Error( 'rest_comment_invalid_type', __( 'Sorry, you cannot change the comment type.' ), array( 'status' => 404 ) );
286
+ }
287
+
288
+ $prepared_args = $this->prepare_item_for_database( $request );
289
+
290
+ if ( empty( $prepared_args ) && isset( $request['status'] ) ) {
291
+ // Only the comment status is being changed.
292
+ $change = $this->handle_status_param( $request['status'], $comment );
293
+ if ( ! $change ) {
294
+ return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment status failed.' ), array( 'status' => 500 ) );
295
+ }
296
+ } else {
297
+ $prepared_args['comment_ID'] = $id;
298
+
299
+ $updated = wp_update_comment( $prepared_args );
300
+ if ( 0 === $updated ) {
301
+ return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment failed.' ), array( 'status' => 500 ) );
302
+ }
303
+
304
+ if ( isset( $request['status'] ) ) {
305
+ $this->handle_status_param( $request['status'], $comment );
306
+ }
307
+ }
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'];
332
+ $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
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 ) {
356
+ return new WP_Error( 'rest_trash_not_supported', __( 'The comment does not support trashing.' ), array( 'status' => 501 ) );
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
+
369
+
370
+ /**
371
+ * Check if a given request has access to read comments
372
+ *
373
+ * @param WP_REST_Request $request Full details about the request.
374
+ * @return bool|WP_Error
375
+ */
376
+ public function get_items_permissions_check( $request ) {
377
+
378
+ // If the post id is specified, check that we can read the post
379
+ if ( isset( $request['post'] ) ) {
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;
392
+ }
393
+
394
+ /**
395
+ * Check if a given request has access to read the comment
396
+ *
397
+ * @param WP_REST_Request $request Full details about the request.
398
+ * @return bool|WP_Error
399
+ */
400
+ public function get_item_permissions_check( $request ) {
401
+ $id = (int) $request['id'];
402
+
403
+ $comment = get_comment( $id );
404
+
405
+ if ( ! $comment ) {
406
+ return true;
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;
424
+ }
425
+
426
+ /**
427
+ * Check if a given request has access to create a comment
428
+ *
429
+ * @param WP_REST_Request $request Full details about the request.
430
+ * @return bool|WP_Error
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 ) ) {
459
+ return new WP_Error( 'rest_comment_closed', __( 'Sorry, comments are closed on this post.' ), array( 'status' => 403 ) );
460
+ }
461
+ }
462
+
463
+ return true;
464
+ }
465
+
466
+ /**
467
+ * Check if a given request has access to update a comment
468
+ *
469
+ * @param WP_REST_Request $request Full details about the request.
470
+ * @return bool|WP_Error
471
+ */
472
+ public function update_item_permissions_check( $request ) {
473
+
474
+ $id = (int) $request['id'];
475
+
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;
483
+ }
484
+
485
+ /**
486
+ * Check if a given request has access to delete a comment
487
+ *
488
+ * @param WP_REST_Request $request Full details about the request.
489
+ * @return bool|WP_Error
490
+ */
491
+ public function delete_item_permissions_check( $request ) {
492
+ return $this->update_item_permissions_check( $request );
493
+ }
494
+
495
+ /**
496
+ * Prepare a single comment output for response.
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(
504
+ 'id' => (int) $comment->comment_ID,
505
+ 'post' => (int) $comment->comment_post_ID,
506
+ 'parent' => (int) $comment->comment_parent,
507
+ 'author' => (int) $comment->user_id,
508
+ 'author_name' => $comment->comment_author,
509
+ 'author_email' => $comment->comment_author_email,
510
+ 'author_url' => $comment->comment_author_url,
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,
519
+ ),
520
+ 'karma' => (int) $comment->comment_karma,
521
+ 'link' => get_comment_link( $comment ),
522
+ 'status' => $this->prepare_status_response( $comment->comment_approved ),
523
+ 'type' => get_comment_type( $comment->comment_ID ),
524
+ );
525
+
526
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
527
+ $data = $this->filter_response_by_context( $data, $context );
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
+ /**
539
+ * Prepare links for the request.
540
+ *
541
+ * @param object $comment Comment object.
542
+ * @return array Links for the given comment.
543
+ */
544
+ protected function prepare_links( $comment ) {
545
+ $links = array(
546
+ 'self' => array(
547
+ 'href' => rest_url( '/wp/v2/comments/' . $comment->comment_ID ),
548
+ ),
549
+ 'collection' => array(
550
+ 'href' => rest_url( '/wp/v2/comments' ),
551
+ ),
552
+ );
553
+
554
+ if ( 0 !== (int) $comment->user_id ) {
555
+ $links['author'] = array(
556
+ 'href' => rest_url( '/wp/v2/users/' . $comment->user_id ),
557
+ 'embeddable' => true,
558
+ );
559
+ }
560
+
561
+ if ( 0 !== (int) $comment->comment_post_ID ) {
562
+ $post = get_post( $comment->comment_post_ID );
563
+ if ( ! empty( $post->ID ) ) {
564
+ $posts_controller = new WP_REST_Posts_Controller( $post->post_type );
565
+ $base = $posts_controller->get_post_type_base( $post->post_type );
566
+
567
+ $links['up'] = array(
568
+ 'href' => rest_url( '/wp/v2/' . $base . '/' . $comment->comment_post_ID ),
569
+ 'embeddable' => true,
570
+ 'post_type' => $post->post_type,
571
+ );
572
+ }
573
+ }
574
+
575
+ if ( 0 !== (int) $comment->comment_parent ) {
576
+ $links['in-reply-to'] = array(
577
+ 'href' => rest_url( sprintf( '/wp/v2/comments/%d', (int) $comment->comment_parent ) ),
578
+ 'embeddable' => true,
579
+ );
580
+ }
581
+
582
+ return $links;
583
+ }
584
+
585
+ /**
586
+ * Filter query parameters for comments collection endpoint.
587
+ *
588
+ * Prepares arguments before passing them along to WP_Comment_Query.
589
+ *
590
+ * @param WP_REST_Request $request Request object.
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 );
608
+
609
+ if ( current_user_can( 'edit_posts' ) ) {
610
+ $protected_args = array(
611
+ 'user' => $request['user'] ? $request['user'] : '',
612
+ 'status' => $request['status'],
613
+ 'type' => isset( $request['type'] ) ? $request['type'] : '',
614
+ 'author_email' => isset( $request['author_email'] ) ? $request['author_email'] : '',
615
+ 'karma' => isset( $request['karma'] ) ? $request['karma'] : '',
616
+ 'post_author' => isset( $request['post_author'] ) ? $request['post_author'] : '',
617
+ 'post_name' => isset( $request['post_slug'] ) ? $request['post_slug'] : '',
618
+ 'post_parent' => isset( $request['post_parent'] ) ? $request['post_parent'] : '',
619
+ 'post_status' => isset( $request['post_status'] ) ? $request['post_status'] : '',
620
+ 'post_type' => isset( $request['post_type'] ) ? $request['post_type'] : '',
621
+ );
622
+
623
+ $prepared_args = array_merge( $prepared_args, $protected_args );
624
+ }
625
+
626
+ return $prepared_args;
627
+ }
628
+
629
+ /**
630
+ * Prepend internal property prefix to query parameters to match our response fields.
631
+ *
632
+ * @param string $query_param
633
+ * @return string $normalized
634
+ */
635
+ protected function normalize_query_param( $query_param ) {
636
+ $prefix = 'comment_';
637
+
638
+ switch ( $query_param ) {
639
+ case 'id':
640
+ $normalized = $prefix . 'ID';
641
+ break;
642
+ case 'post':
643
+ $normalized = $prefix . 'post_ID';
644
+ break;
645
+ case 'parent':
646
+ $normalized = $prefix . 'parent';
647
+ break;
648
+ default:
649
+ $normalized = $prefix . $query_param;
650
+ break;
651
+ }
652
+
653
+ return $normalized;
654
+ }
655
+
656
+ /**
657
+ * Check comment_approved to set comment status for single comment output.
658
+ *
659
+ * @param string|int $comment_approved
660
+ * @return string $status
661
+ */
662
+ protected function prepare_status_response( $comment_approved ) {
663
+
664
+ switch ( $comment_approved ) {
665
+ case 'hold':
666
+ case '0':
667
+ $status = 'hold';
668
+ break;
669
+
670
+ case 'approve':
671
+ case '1':
672
+ $status = 'approved';
673
+ break;
674
+
675
+ case 'spam':
676
+ case 'trash':
677
+ default:
678
+ $status = $comment_approved;
679
+ break;
680
+ }
681
+
682
+ return $status;
683
+ }
684
+
685
+ /**
686
+ * Prepare a single comment to be inserted into the database.
687
+ *
688
+ * @param WP_REST_Request $request Request object.
689
+ * @return array|WP_Error $prepared_comment
690
+ */
691
+ protected function prepare_item_for_database( $request ) {
692
+ $prepared_comment = array();
693
+
694
+ if ( isset( $request['content'] ) ) {
695
+ $prepared_comment['comment_content'] = $request['content'];
696
+ }
697
+
698
+ if ( isset( $request['post'] ) ) {
699
+ $prepared_comment['comment_post_ID'] = (int) $request['post'];
700
+ }
701
+
702
+ if ( isset( $request['parent'] ) ) {
703
+ $prepared_comment['comment_parent'] = $request['parent'];
704
+ }
705
+
706
+ if ( isset( $request['author'] ) ) {
707
+ $prepared_comment['user_id'] = $request['author'];
708
+ }
709
+
710
+ if ( isset( $request['author_name'] ) ) {
711
+ $prepared_comment['comment_author'] = $request['author_name'];
712
+ }
713
+
714
+ if ( isset( $request['author_email'] ) ) {
715
+ $prepared_comment['comment_author_email'] = $request['author_email'];
716
+ }
717
+
718
+ if ( isset( $request['author_url'] ) ) {
719
+ $prepared_comment['comment_author_url'] = $request['author_url'];
720
+ }
721
+
722
+ if ( isset( $request['type'] ) ) {
723
+ $prepared_comment['comment_type'] = $request['type'];
724
+ }
725
+
726
+ if ( isset( $request['karma'] ) ) {
727
+ $prepared_comment['comment_karma'] = $request['karma'] ;
728
+ }
729
+
730
+ if ( ! empty( $request['date'] ) ) {
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
+
749
+ return apply_filters( 'rest_preprocess_comment', $prepared_comment, $request );
750
+ }
751
+
752
+ /**
753
+ * Get the Comment's schema, conforming to JSON Schema
754
+ *
755
+ * @return array
756
+ */
757
+ public function get_item_schema() {
758
+ $avatar_properties = array();
759
+
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
+ }
768
+
769
+ $schema = array(
770
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
771
+ 'title' => 'comment',
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
+ );
885
+ return $this->add_additional_fields_schema( $schema );
886
+ }
887
+
888
+ /**
889
+ * Get the query params for collections
890
+ *
891
+ * @return array
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
+ );
968
+ return $query_params;
969
+ }
970
+
971
+ /**
972
+ * Set the comment_status of a given comment object when creating or updating a comment.
973
+ *
974
+ * @param string|int $new_status
975
+ * @param object $comment
976
+ * @return boolean $changed
977
+ */
978
+ protected function handle_status_param( $new_status, $comment ) {
979
+ $old_status = wp_get_comment_status( $comment->comment_ID );
980
+
981
+ if ( $new_status === $old_status ) {
982
+ return false;
983
+ }
984
+
985
+ switch ( $new_status ) {
986
+ case 'approved' :
987
+ case 'approve':
988
+ case '1':
989
+ $changed = wp_set_comment_status( $comment->comment_ID, 'approve' );
990
+ break;
991
+ case 'hold':
992
+ case '0':
993
+ $changed = wp_set_comment_status( $comment->comment_ID, 'hold' );
994
+ break;
995
+ case 'spam' :
996
+ $changed = wp_spam_comment( $comment->comment_ID );
997
+ break;
998
+ case 'unspam' :
999
+ $changed = wp_unspam_comment( $comment->comment_ID );
1000
+ break;
1001
+ case 'trash' :
1002
+ $changed = wp_trash_comment( $comment->comment_ID );
1003
+ break;
1004
+ case 'untrash' :
1005
+ $changed = wp_untrash_comment( $comment->comment_ID );
1006
+ break;
1007
+ default :
1008
+ $changed = false;
1009
+ break;
1010
+ }
1011
+
1012
+ return $changed;
1013
+ }
1014
+
1015
+ /**
1016
+ * Check if we can read a post.
1017
+ *
1018
+ * Correctly handles posts with the inherit status.
1019
+ *
1020
+ * @param WP_Post $post Post Object.
1021
+ * @return boolean Can we read it?
1022
+ */
1023
+ protected function check_read_post_permission( $post ) {
1024
+ $posts_controller = new WP_REST_Posts_Controller( $post->post_type );
1025
+
1026
+ return $posts_controller->check_read_permission( $post );
1027
+ }
1028
+
1029
+ /**
1030
+ * Check if we can read a comment.
1031
+ *
1032
+ * @param object $comment Comment object.
1033
+ * @return boolean Can we read it?
1034
+ */
1035
+ protected function check_read_permission( $comment ) {
1036
+
1037
+ if ( 1 === (int) $comment->comment_approved ) {
1038
+ return true;
1039
+ }
1040
+
1041
+ if ( 0 === get_current_user_id() ) {
1042
+ return false;
1043
+ }
1044
+
1045
+ if ( ! empty( $comment->user_id ) && get_current_user_id() === (int) $comment->user_id ) {
1046
+ return true;
1047
+ }
1048
+
1049
+ return current_user_can( 'edit_comment', $comment->comment_ID );
1050
+ }
1051
+
1052
+ /**
1053
+ * Check if we can edit or delete a comment.
1054
+ *
1055
+ * @param object $comment Comment object.
1056
+ * @return boolean Can we edit or delete it?
1057
+ */
1058
+ protected function check_edit_permission( $comment ) {
1059
+ if ( 0 === (int) get_current_user_id() ) {
1060
+ return false;
1061
+ }
1062
+
1063
+ if ( ! current_user_can( 'moderate_comments' ) ) {
1064
+ return false;
1065
+ }
1066
+
1067
+ return current_user_can( 'edit_comment', $comment->comment_ID );
1068
+ }
1069
+ }
lib/endpoints/class-wp-rest-controller.php ADDED
@@ -0,0 +1,505 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+
4
+ abstract class WP_REST_Controller {
5
+
6
+ /**
7
+ * Register the routes for the objects of the controller.
8
+ */
9
+ public function register_routes() {
10
+ _doing_it_wrong( 'WP_REST_Controller::register_routes', __( 'The register_routes() method must be overriden' ), 'WPAPI-2.0' );
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
+ /**
135
+ * Prepare a response for inserting into a collection.
136
+ *
137
+ * @param WP_REST_Response $response Response object.
138
+ * @return array Response data, ready for insertion into collection data.
139
+ */
140
+ public function prepare_response_for_collection( $response ) {
141
+ if ( ! ( $response instanceof WP_REST_Response ) ) {
142
+ return $response;
143
+ }
144
+
145
+ $data = (array) $response->get_data();
146
+ $links = WP_REST_Server::get_response_links( $response );
147
+ if ( ! empty( $links ) ) {
148
+ $data['_links'] = $links;
149
+ }
150
+
151
+ return $data;
152
+ }
153
+
154
+ /**
155
+ * Filter a response based on the context defined in the schema
156
+ *
157
+ * @param array $data
158
+ * @param string $context
159
+ * @return array
160
+ */
161
+ public function filter_response_by_context( $data, $context ) {
162
+
163
+ $schema = $this->get_item_schema();
164
+ foreach ( $data as $key => $value ) {
165
+ if ( empty( $schema['properties'][ $key ] ) || empty( $schema['properties'][ $key ]['context'] ) ) {
166
+ continue;
167
+ }
168
+
169
+ if ( ! in_array( $context, $schema['properties'][ $key ]['context'] ) ) {
170
+ unset( $data[ $key ] );
171
+ }
172
+
173
+ if ( 'object' === $schema['properties'][ $key ]['type'] && ! empty( $schema['properties'][ $key ]['properties'] ) ) {
174
+ foreach ( $schema['properties'][ $key ]['properties'] as $attribute => $details ) {
175
+ if ( empty( $details['context'] ) ) {
176
+ continue;
177
+ }
178
+ if ( ! in_array( $context, $details['context'] ) ) {
179
+ unset( $data[ $key ][ $attribute ] );
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ return $data;
186
+ }
187
+
188
+ /**
189
+ * Get the item's schema, conforming to JSON Schema
190
+ *
191
+ * @return array
192
+ */
193
+ public function get_item_schema() {
194
+ return $this->add_additional_fields_schema( array() );
195
+ }
196
+
197
+ /**
198
+ * Get the item's schema for display / public consumption purposes.
199
+ *
200
+ * @return array
201
+ */
202
+ public function get_public_item_schema() {
203
+
204
+ $schema = $this->get_item_schema();
205
+
206
+ foreach ( $schema['properties'] as &$property ) {
207
+ if ( isset( $property['arg_options'] ) ) {
208
+ unset( $property['arg_options'] );
209
+ }
210
+ }
211
+
212
+ return $schema;
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
+ ),
239
+ );
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
+
251
+ $additional_fields = $this->get_additional_fields();
252
+
253
+ foreach ( $additional_fields as $field_name => $field_options ) {
254
+
255
+ if ( ! $field_options['get_callback'] ) {
256
+ continue;
257
+ }
258
+
259
+ $object[ $field_name ] = call_user_func( $field_options['get_callback'], $object, $field_name, $request );
260
+ }
261
+
262
+ return $object;
263
+ }
264
+
265
+ /**
266
+ * Update the values of additional fields added to a data object.
267
+ *
268
+ * @param array $object
269
+ * @param WP_REST_Request $request
270
+ */
271
+ protected function update_additional_fields_for_object( $object, $request ) {
272
+
273
+ $additional_fields = $this->get_additional_fields();
274
+
275
+ foreach ( $additional_fields as $field_name => $field_options ) {
276
+
277
+ if ( ! $field_options['update_callback'] ) {
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'] ) ) {
299
+ return $schema;
300
+ }
301
+
302
+ /**
303
+ * Can't use $this->get_object_type otherwise we cause an inf loop
304
+ */
305
+ $object_type = $schema['title'];
306
+
307
+ $additional_fields = $this->get_additional_fields( $object_type );
308
+
309
+ foreach ( $additional_fields as $field_name => $field_options ) {
310
+ if ( ! $field_options['schema'] ) {
311
+ continue;
312
+ }
313
+
314
+ $schema['properties'][ $field_name ] = $field_options['schema'];
315
+ }
316
+
317
+ return $schema;
318
+ }
319
+
320
+ /**
321
+ * Get all the registered additional fields for a given object-type
322
+ *
323
+ * @param string $object_type
324
+ * @return array
325
+ */
326
+ protected function get_additional_fields( $object_type = null ) {
327
+
328
+ if ( ! $object_type ) {
329
+ $object_type = $this->get_object_type();
330
+ }
331
+
332
+ if ( ! $object_type ) {
333
+ return array();
334
+ }
335
+
336
+ global $wp_rest_additional_fields;
337
+
338
+ if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) {
339
+ return array();
340
+ }
341
+
342
+ return $wp_rest_additional_fields[ $object_type ];
343
+ }
344
+
345
+ /**
346
+ * Get the object type this controller is responsible for managing.
347
+ *
348
+ * @return string
349
+ */
350
+ protected function get_object_type() {
351
+ $schema = $this->get_item_schema();
352
+
353
+ if ( ! $schema || ! isset( $schema['title'] ) ) {
354
+ return null;
355
+ }
356
+
357
+ return $schema['title'];
358
+ }
359
+
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();
372
+ $endpoint_args = array();
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
+ }
380
+
381
+ $endpoint_args[ $field_id ] = array(
382
+ 'validate_callback' => array( $this, 'validate_schema_property' ),
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
+ }
399
+
400
+ return $endpoint_args;
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
408
+ * @param string $parameter
409
+ * @return WP_Error|bool
410
+ */
411
+ public function validate_schema_property( $value, $request, $parameter ) {
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 ) {
419
+ return true;
420
+ }
421
+
422
+ $schema = $this->get_item_schema();
423
+
424
+ if ( ! isset( $schema['properties'][ $parameter ] ) ) {
425
+ return true;
426
+ }
427
+
428
+ $property = $schema['properties'][ $parameter ];
429
+
430
+ if ( ! empty( $property['enum'] ) ) {
431
+ if ( ! in_array( $value, $property['enum'] ) ) {
432
+ return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not one of %s' ), $parameter, implode( ', ', $property['enum'] ) ) );
433
+ }
434
+ }
435
+
436
+ if ( 'integer' === $property['type'] && ! is_numeric( $value ) ) {
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
+
444
+ if ( isset( $property['format'] ) ) {
445
+ switch ( $property['format'] ) {
446
+ case 'date-time' :
447
+ if ( ! rest_parse_date( $value ) ) {
448
+ return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ) );
449
+ }
450
+ break;
451
+
452
+ case 'email' :
453
+ if ( ! is_email( $value ) ) {
454
+ return new WP_Error( 'rest_invalid_email', __( 'The email address you provided is invalid.' ) );
455
+ }
456
+ break;
457
+ }
458
+ }
459
+
460
+ return true;
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
468
+ * @param string $parameter
469
+ * @return WP_Error|bool
470
+ */
471
+ public function sanitize_schema_property( $value, $request, $parameter ) {
472
+
473
+ $schema = $this->get_item_schema();
474
+
475
+ if ( ! isset( $schema['properties'][ $parameter ] ) ) {
476
+ return true;
477
+ }
478
+
479
+ $property = $schema['properties'][ $parameter ];
480
+
481
+ if ( 'integer' === $property['type'] ) {
482
+ return intval( $value );
483
+ }
484
+
485
+ if ( isset( $property['format'] ) ) {
486
+ switch ( $property['format'] ) {
487
+ case 'date-time' :
488
+ return sanitize_text_field( $value );
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
+ }
496
+ return sanitize_text_field( $value );
497
+
498
+ case 'uri' :
499
+ return esc_url_raw( $value );
500
+ }
501
+ }
502
+
503
+ return $value;
504
+ }
505
+ }
lib/endpoints/class-wp-rest-meta-controller.php ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Metadata base class.
4
+ */
5
+ abstract class WP_REST_Meta_Controller extends WP_REST_Controller {
6
+ /**
7
+ * Associated object type.
8
+ *
9
+ * @var string Type slug ("post", "user", or "comment")
10
+ */
11
+ protected $parent_type = null;
12
+
13
+ /**
14
+ * Base path for parent meta type endpoints.
15
+ *
16
+ * @var string
17
+ */
18
+ protected $parent_base = null;
19
+
20
+ /**
21
+ * Construct the API handler object.
22
+ */
23
+ public function __construct() {
24
+ if ( empty( $this->parent_type ) ) {
25
+ _doing_it_wrong( 'WP_REST_Meta_Controller::__construct', __( 'The object type must be overridden' ), 'WPAPI-2.0' );
26
+ return;
27
+ }
28
+ if ( empty( $this->parent_base ) ) {
29
+ _doing_it_wrong( 'WP_REST_Meta_Controller::__construct', __( 'The parent base must be overridden' ), 'WPAPI-2.0' );
30
+ return;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Register the meta-related routes.
36
+ */
37
+ public function register_routes() {
38
+ register_rest_route( 'wp/v2', '/' . $this->parent_base . '/(?P<parent_id>[\d]+)/meta', array(
39
+ array(
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(
63
+ 'methods' => WP_REST_Server::READABLE,
64
+ 'callback' => array( $this, 'get_item' ),
65
+ 'permission_callback' => array( $this, 'get_item_permissions_check' ),
66
+ 'args' => array(
67
+ 'context' => 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
+
94
+ /**
95
+ * Get the meta schema, conforming to JSON Schema
96
+ *
97
+ * @return array
98
+ */
99
+ public function get_item_schema() {
100
+ $schema = array(
101
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
102
+ 'title' => 'meta',
103
+ 'type' => 'object',
104
+ /*
105
+ * Base properties for every Post
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
+ ),
123
+ ),
124
+ );
125
+ return $schema;
126
+ }
127
+
128
+ /**
129
+ * Get the meta ID column for the relevant table.
130
+ *
131
+ * @return string
132
+ */
133
+ protected function get_id_column() {
134
+ return ( 'user' === $this->parent_type ) ? 'umeta_id' : 'meta_id';
135
+ }
136
+
137
+ /**
138
+ * Get the object (parent) ID column for the relevant table.
139
+ *
140
+ * @return string
141
+ */
142
+ protected function get_parent_column() {
143
+ return ( 'user' === $this->parent_type ) ? 'user_id' : 'post_id';
144
+ }
145
+
146
+ /**
147
+ * Retrieve custom fields for object.
148
+ *
149
+ * @param WP_REST_Request $request
150
+ * @return WP_REST_Request|WP_Error List of meta object data on success, WP_Error otherwise
151
+ */
152
+ public function get_items( $request ) {
153
+ $parent_id = (int) $request['parent_id'];
154
+
155
+ global $wpdb;
156
+ $table = _get_meta_table( $this->parent_type );
157
+ $parent_column = $this->get_parent_column();
158
+ $id_column = $this->get_id_column();
159
+
160
+ // @codingStandardsIgnoreStart
161
+ $results = $wpdb->get_results( $wpdb->prepare( "SELECT $id_column, $parent_column, meta_key, meta_value FROM $table WHERE $parent_column = %d", $parent_id ) );
162
+ // @codingStandardsIgnoreEnd
163
+
164
+ $meta = array();
165
+
166
+ foreach ( $results as $row ) {
167
+ $value = $this->prepare_item_for_response( $row, $request, true );
168
+
169
+ if ( is_wp_error( $value ) ) {
170
+ continue;
171
+ }
172
+
173
+ $meta[] = $this->prepare_response_for_collection( $value );
174
+ }
175
+
176
+ return rest_ensure_response( $meta );
177
+ }
178
+
179
+ /**
180
+ * Retrieve custom field object.
181
+ *
182
+ * @param WP_REST_Request $request
183
+ * @return WP_REST_Request|WP_Error Meta object data on success, WP_Error otherwise
184
+ */
185
+ public function get_item( $request ) {
186
+ $parent_id = (int) $request['parent_id'];
187
+ $mid = (int) $request['id'];
188
+
189
+ $parent_column = $this->get_parent_column();
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 ) {
197
+ return new WP_Error( 'rest_meta_' . $this->parent_type . '_mismatch', __( 'Meta does not belong to this object' ), array( 'status' => 400 ) );
198
+ }
199
+
200
+ return $this->prepare_item_for_response( $meta, $request );
201
+ }
202
+
203
+ /**
204
+ * Prepares meta data for return as an object.
205
+ *
206
+ * @param stdClass $data Metadata row from database
207
+ * @param WP_REST_Request $request
208
+ * @param boolean $is_raw Is the value field still serialized? (False indicates the value has been unserialized)
209
+ * @return WP_REST_Response|WP_Error Meta object data on success, WP_Error otherwise
210
+ */
211
+ public function prepare_item_for_response( $data, $request, $is_raw = false ) {
212
+ $id_column = $this->get_id_column();
213
+ $id = $data->$id_column;
214
+ $key = $data->meta_key;
215
+ $value = $data->meta_value;
216
+
217
+ // Don't expose protected fields.
218
+ if ( is_protected_meta( $key ) ) {
219
+ return new WP_Error( 'rest_meta_protected', sprintf( __( '%s is marked as a protected field.' ), $key ), array( 'status' => 403 ) );
220
+ }
221
+
222
+ // Normalize serialized strings
223
+ if ( $is_raw && is_serialized_string( $value ) ) {
224
+ $value = unserialize( $value );
225
+ }
226
+
227
+ // Don't expose serialized data
228
+ if ( is_serialized( $value ) || ! is_string( $value ) ) {
229
+ return new WP_Error( 'rest_meta_protected', sprintf( __( '%s contains serialized data.' ), $key ), array( 'status' => 403 ) );
230
+ }
231
+
232
+ $meta = array(
233
+ 'id' => (int) $id,
234
+ 'key' => $key,
235
+ 'value' => $value,
236
+ );
237
+
238
+ $response = rest_ensure_response( $meta );
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
+
245
+ /**
246
+ * Add meta to an object.
247
+ *
248
+ * @param WP_REST_Request $request
249
+ * @return WP_REST_Response|WP_Error
250
+ */
251
+ public function update_item( $request ) {
252
+ $parent_id = (int) $request['parent_id'];
253
+ $mid = (int) $request['id'];
254
+
255
+ $parent_column = $this->get_parent_column();
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 ) {
263
+ return new WP_Error( 'rest_meta_' . $this->parent_type . '_mismatch', __( 'Meta does not belong to this object' ), array( 'status' => 400 ) );
264
+ }
265
+
266
+ if ( ! isset( $request['key'] ) && ! isset( $request['value'] ) ) {
267
+ return new WP_Error( 'rest_meta_data_invalid', __( 'Invalid meta parameters.' ), array( 'status' => 400 ) );
268
+ }
269
+ if ( isset( $request['key'] ) ) {
270
+ $key = $request['key'];
271
+ } else {
272
+ $key = $current->meta_key;
273
+ }
274
+
275
+ if ( isset( $request['value'] ) ) {
276
+ $value = $request['value'];
277
+ } else {
278
+ $value = $current->meta_value;
279
+ }
280
+
281
+ if ( ! $key ) {
282
+ return new WP_Error( 'rest_meta_invalid_key', __( 'Invalid meta key.' ), array( 'status' => 400 ) );
283
+ }
284
+
285
+ // for now let's not allow updating of arrays, objects or serialized values.
286
+ if ( ! $this->is_valid_meta_data( $current->meta_value ) ) {
287
+ $code = ( $this->parent_type === 'post' ) ? 'rest_post_invalid_action' : 'rest_meta_invalid_action';
288
+ return new WP_Error( $code, __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) );
289
+ }
290
+
291
+ if ( ! $this->is_valid_meta_data( $value ) ) {
292
+ $code = ( $this->parent_type === 'post' ) ? 'rest_post_invalid_action' : 'rest_meta_invalid_action';
293
+ return new WP_Error( $code, __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) );
294
+ }
295
+
296
+ if ( is_protected_meta( $current->meta_key ) ) {
297
+ return new WP_Error( 'rest_meta_protected', sprintf( __( '%s is marked as a protected field.' ), $current->meta_key ), array( 'status' => 403 ) );
298
+ }
299
+
300
+ if ( is_protected_meta( $key ) ) {
301
+ return new WP_Error( 'rest_meta_protected', sprintf( __( '%s is marked as a protected field.' ), $key ), array( 'status' => 403 ) );
302
+ }
303
+
304
+ // update_metadata_by_mid will return false if these are equal, so check
305
+ // first and pass through
306
+ if ( (string) $value === $current->meta_value && (string) $key === $current->meta_key ) {
307
+ return $this->get_item( $request );
308
+ }
309
+
310
+ if ( ! update_metadata_by_mid( $this->parent_type, $mid, $value, $key ) ) {
311
+ return new WP_Error( 'rest_meta_could_not_update', __( 'Could not update meta.' ), array( 'status' => 500 ) );
312
+ }
313
+
314
+ $request = new WP_REST_Request( 'GET' );
315
+ $request->set_query_params( array(
316
+ 'context' => 'edit',
317
+ 'parent_id' => $parent_id,
318
+ 'id' => $mid,
319
+ ) );
320
+ $response = $this->get_item( $request );
321
+
322
+ return rest_ensure_response( $response );
323
+ }
324
+
325
+ /**
326
+ * Check if the data provided is valid data.
327
+ *
328
+ * Excludes serialized data from being sent via the API.
329
+ *
330
+ * @see https://github.com/WP-API/WP-API/pull/68
331
+ * @param mixed $data Data to be checked
332
+ * @return boolean Whether the data is valid or not
333
+ */
334
+ protected function is_valid_meta_data( $data ) {
335
+ if ( is_array( $data ) || is_object( $data ) || is_serialized( $data ) ) {
336
+ return false;
337
+ }
338
+
339
+ return true;
340
+ }
341
+
342
+ /**
343
+ * Add meta to an object.
344
+ *
345
+ * @param WP_REST_Request $request
346
+ * @return WP_REST_Response|WP_Error
347
+ */
348
+ public function create_item( $request ) {
349
+ $parent_id = (int) $request['parent_id'];
350
+
351
+ if ( ! $this->is_valid_meta_data( $request['value'] ) ) {
352
+ $code = ( $this->parent_type === 'post' ) ? 'rest_post_invalid_action' : 'rest_meta_invalid_action';
353
+
354
+ // for now let's not allow updating of arrays, objects or serialized values.
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
+
369
+ $mid = add_metadata( $this->parent_type, $parent_id, $meta_key, $value );
370
+ if ( ! $mid ) {
371
+ return new WP_Error( 'rest_meta_could_not_add', __( 'Could not add meta.' ), array( 'status' => 400 ) );
372
+ }
373
+
374
+ $request = new WP_REST_Request( 'GET' );
375
+ $request->set_query_params( array(
376
+ 'context' => 'edit',
377
+ 'parent_id' => $parent_id,
378
+ 'id' => $mid,
379
+ ) );
380
+ $response = rest_ensure_response( $this->get_item( $request ) );
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
+ }
388
+
389
+ /**
390
+ * Delete meta from an object.
391
+ *
392
+ * @param WP_REST_Request $request
393
+ * @return WP_REST_Response|WP_Error Message on success, WP_Error otherwise
394
+ */
395
+ public function delete_item( $request ) {
396
+ $parent_id = (int) $request['parent_id'];
397
+ $mid = (int) $request['id'];
398
+ $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
399
+
400
+ // We don't support trashing for this type, error out
401
+ if ( ! $force ) {
402
+ return new WP_Error( 'rest_trash_not_supported', __( 'Meta does not support trashing.' ), array( 'status' => 501 ) );
403
+ }
404
+
405
+ $parent_column = $this->get_parent_column();
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 ) {
413
+ return new WP_Error( 'rest_meta_' . $this->parent_type . '_mismatch', __( 'Meta does not belong to this object' ), array( 'status' => 400 ) );
414
+ }
415
+
416
+ // for now let's not allow updating of arrays, objects or serialized values.
417
+ if ( ! $this->is_valid_meta_data( $current->meta_value ) ) {
418
+ $code = ( $this->parent_type === 'post' ) ? 'rest_post_invalid_action' : 'rest_meta_invalid_action';
419
+ return new WP_Error( $code, __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) );
420
+ }
421
+
422
+ if ( is_protected_meta( $current->meta_key ) ) {
423
+ return new WP_Error( 'rest_meta_protected', sprintf( __( '%s is marked as a protected field.' ), $current->meta_key ), array( 'status' => 403 ) );
424
+ }
425
+
426
+ if ( ! delete_metadata_by_mid( $this->parent_type, $mid ) ) {
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
+ }
lib/endpoints/class-wp-rest-meta-posts-controller.php ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_REST_Meta_Posts_Controller extends WP_REST_Meta_Controller {
4
+ /**
5
+ * Associated object type.
6
+ *
7
+ * @var string Type slug ("post" or "user")
8
+ */
9
+ protected $parent_type = 'post';
10
+
11
+ /**
12
+ * Associated post type name.
13
+ *
14
+ * @var string
15
+ */
16
+ protected $parent_post_type;
17
+
18
+ /**
19
+ * Associated post type controller class object.
20
+ *
21
+ * @var WP_REST_Posts_Controller
22
+ */
23
+ protected $parent_controller;
24
+
25
+ /**
26
+ * Base path for post type endpoints.
27
+ *
28
+ * @var string
29
+ */
30
+ protected $parent_base;
31
+
32
+ public function __construct( $parent_post_type ) {
33
+ $this->parent_post_type = $parent_post_type;
34
+ $this->parent_controller = new WP_REST_Posts_Controller( $this->parent_post_type );
35
+ $this->parent_base = $this->parent_controller->get_post_type_base( $this->parent_post_type );
36
+ }
37
+
38
+ /**
39
+ * Check if a given request has access to get meta for a post.
40
+ *
41
+ * @param WP_REST_Request $request Full data about the request.
42
+ * @return WP_Error|boolean
43
+ */
44
+ public function get_items_permissions_check( $request ) {
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
+ }
61
+
62
+ /**
63
+ * Check if a given request has access to get a specific meta entry for a post.
64
+ *
65
+ * @param WP_REST_Request $request Full data about the request.
66
+ * @return WP_Error|boolean
67
+ */
68
+ public function get_item_permissions_check( $request ) {
69
+ return $this->get_items_permissions_check( $request );
70
+ }
71
+
72
+ /**
73
+ * Check if a given request has access to create a meta entry for a post.
74
+ *
75
+ * @param WP_REST_Request $request Full data about the request.
76
+ * @return WP_Error|boolean
77
+ */
78
+ public function create_item_permissions_check( $request ) {
79
+ return $this->get_items_permissions_check( $request );
80
+ }
81
+
82
+ /**
83
+ * Check if a given request has access to update a meta entry for a post.
84
+ *
85
+ * @param WP_REST_Request $request Full data about the request.
86
+ * @return WP_Error|boolean
87
+ */
88
+ public function update_item_permissions_check( $request ) {
89
+ return $this->get_items_permissions_check( $request );
90
+ }
91
+
92
+ /**
93
+ * Check if a given request has access to delete meta for a post.
94
+ *
95
+ * @param WP_REST_Request $request Full details about the request.
96
+ * @return WP_Error|boolean
97
+ */
98
+ public function delete_item_permissions_check( $request ) {
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
+ }
115
+ }
lib/endpoints/class-wp-rest-post-statuses-controller.php ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_REST_Post_Statuses_Controller extends WP_REST_Controller {
4
+
5
+ /**
6
+ * Register the routes for the objects of the controller.
7
+ */
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
+
26
+ /**
27
+ * Get all post statuses, depending on user context
28
+ *
29
+ * @param WP_REST_Request $request
30
+ * @return array|WP_Error
31
+ */
32
+ public function get_items( $request ) {
33
+ $data = array();
34
+ if ( is_user_logged_in() ) {
35
+ $statuses = get_post_stati( array( 'internal' => false ), 'object' );
36
+ } else {
37
+ $statuses = get_post_stati( array( 'public' => true ), 'object' );
38
+ }
39
+ foreach ( $statuses as $obj ) {
40
+ $status = $this->prepare_item_for_response( $obj, $request );
41
+ if ( is_wp_error( $status ) ) {
42
+ continue;
43
+ }
44
+ $data[ $obj->name ] = $this->prepare_response_for_collection( $status );
45
+ }
46
+ return $data;
47
+ }
48
+
49
+ /**
50
+ * Get a specific post status
51
+ *
52
+ * @param WP_REST_Request $request
53
+ * @return array|WP_Error
54
+ */
55
+ public function get_item( $request ) {
56
+ $obj = get_post_status_object( $request['status'] );
57
+ if ( empty( $obj ) ) {
58
+ return new WP_Error( 'rest_status_invalid', __( 'Invalid status.' ), array( 'status' => 404 ) );
59
+ }
60
+ return $this->prepare_item_for_response( $obj, $request );
61
+ }
62
+
63
+ /**
64
+ * Prepare a post status object for serialization
65
+ *
66
+ * @param stdClass $status Post status data
67
+ * @param WP_REST_Request $request
68
+ * @return WP_REST_Response Post status data
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(
76
+ 'name' => $status->label,
77
+ 'private' => (bool) $status->private,
78
+ 'protected' => (bool) $status->protected,
79
+ 'public' => (bool) $status->public,
80
+ 'queryable' => (bool) $status->publicly_queryable,
81
+ 'show_in_list' => (bool) $status->show_in_admin_all_list,
82
+ 'slug' => $status->name,
83
+ );
84
+
85
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
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
+ /**
103
+ * Get the Post status' schema, conforming to JSON Schema
104
+ *
105
+ * @return array
106
+ */
107
+ public function get_item_schema() {
108
+ $schema = array(
109
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
110
+ 'title' => 'status',
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
+ ),
148
+ ),
149
+ );
150
+ return $this->add_additional_fields_schema( $schema );
151
+ }
152
+
153
+ }
lib/endpoints/class-wp-rest-post-types-controller.php ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_REST_Post_Types_Controller extends WP_REST_Controller {
4
+
5
+ /**
6
+ * Register the routes for the objects of the controller.
7
+ */
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
+
31
+ /**
32
+ * Get all public post types
33
+ *
34
+ * @param WP_REST_Request $request
35
+ * @return array|WP_Error
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
+ }
48
+
49
+ /**
50
+ * Get a specific post type
51
+ *
52
+ * @param WP_REST_Request $request
53
+ * @return array|WP_Error
54
+ */
55
+ public function get_item( $request ) {
56
+ $obj = get_post_type_object( $request['type'] );
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
+
63
+ /**
64
+ * Prepare a post type object for serialization
65
+ *
66
+ * @param stdClass $post_type Post type data
67
+ * @param WP_REST_Request $request
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,
78
+ 'labels' => $post_type->labels,
79
+ 'name' => $post_type->label,
80
+ 'slug' => $post_type->name,
81
+ );
82
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
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
+ /**
90
+ * Get the Post type's schema, conforming to JSON Schema
91
+ *
92
+ * @return array
93
+ */
94
+ public function get_item_schema() {
95
+ $schema = array(
96
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
97
+ 'title' => 'type',
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
+ }
lib/endpoints/class-wp-rest-posts-controller.php ADDED
@@ -0,0 +1,1454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_REST_Posts_Controller extends WP_REST_Controller {
4
+
5
+ protected $post_type;
6
+
7
+ public function __construct( $post_type ) {
8
+ $this->post_type = $post_type;
9
+ }
10
+
11
+ /**
12
+ * Register the routes for the objects of the controller.
13
+ */
14
+ public function register_routes() {
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(
53
+ 'methods' => WP_REST_Server::READABLE,
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,
70
+ 'callback' => array( $this, 'delete_item' ),
71
+ 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
72
+ 'args' => array(
73
+ 'force' => array(
74
+ 'default' => false,
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 );
109
+
110
+ $posts_query = new WP_Query();
111
+ $query_result = $posts_query->query( $query_args );
112
+
113
+ $posts = array();
114
+ foreach ( $query_result as $post ) {
115
+ if ( ! $this->check_read_permission( $post ) ) {
116
+ continue;
117
+ }
118
+
119
+ $data = $this->prepare_item_for_response( $post, $request );
120
+ $posts[] = $this->prepare_response_for_collection( $data );
121
+ }
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
+ }
146
+
147
+ return $response;
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 ) {
157
+ $id = (int) $request['id'];
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 );
165
+ $response = rest_ensure_response( $data );
166
+
167
+ $response->link_header( 'alternate', get_permalink( $id ), array( 'type' => 'text/html' ) );
168
+
169
+ return $response;
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 ) {
179
+ if ( ! empty( $request['id'] ) ) {
180
+ return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.' ), array( 'status' => 400 ) );
181
+ }
182
+
183
+ $post = $this->prepare_item_for_database( $request );
184
+ if ( is_wp_error( $post ) ) {
185
+ return $post;
186
+ }
187
+
188
+ $post->post_type = $this->post_type;
189
+ $post_id = wp_insert_post( $post, true );
190
+
191
+ if ( is_wp_error( $post_id ) ) {
192
+
193
+ if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) {
194
+ $post_id->add_data( array( 'status' => 500 ) );
195
+ } else {
196
+ $post_id->add_data( array( 'status' => 400 ) );
197
+ }
198
+ return $post_id;
199
+ }
200
+ $post->ID = $post_id;
201
+
202
+ $schema = $this->get_item_schema();
203
+
204
+ if ( ! empty( $schema['properties']['sticky'] ) ) {
205
+ if ( ! empty( $request['sticky'] ) ) {
206
+ stick_post( $post_id );
207
+ } else {
208
+ unstick_post( $post_id );
209
+ }
210
+ }
211
+
212
+ if ( ! empty( $schema['properties']['featured_image'] ) && isset( $request['featured_image'] ) ) {
213
+ $this->handle_featured_image( $request['featured_image'], $post->ID );
214
+ }
215
+
216
+ if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
217
+ set_post_format( $post, $request['format'] );
218
+ }
219
+
220
+ if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
221
+ $this->handle_template( $request['template'], $post->ID );
222
+ }
223
+
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 ) );
240
+
241
+ return $response;
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 ) );
267
+ } else {
268
+ $post_id->add_data( array( 'status' => 400 ) );
269
+ }
270
+ return $post_id;
271
+ }
272
+
273
+ $schema = $this->get_item_schema();
274
+
275
+ if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
276
+ set_post_format( $post, $request['format'] );
277
+ }
278
+
279
+ if ( ! empty( $schema['properties']['featured_image'] ) && isset( $request['featured_image'] ) ) {
280
+ $this->handle_featured_image( $request['featured_image'], $post_id );
281
+ }
282
+
283
+ if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) {
284
+ if ( ! empty( $request['sticky'] ) ) {
285
+ stick_post( $post_id );
286
+ } else {
287
+ unstick_post( $post_id );
288
+ }
289
+ }
290
+
291
+ if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
292
+ $this->handle_template( $request['template'], $post->ID );
293
+ }
294
+
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'];
318
+ $force = (bool) $request['force'];
319
+
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 );
327
+ if ( $post->post_type === 'attachment' ) {
328
+ $supports_trash = $supports_trash && MEDIA_TRASH;
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
+ }
360
+
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
378
+ */
379
+ public function get_item_permissions_check( $request ) {
380
+
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 ) {
388
+ return $this->check_read_permission( $post );
389
+ }
390
+
391
+ return true;
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
399
+ */
400
+ public function create_item_permissions_check( $request ) {
401
+
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
424
+ */
425
+ public function update_item_permissions_check( $request ) {
426
+
427
+ $post = get_post( $request['id'] );
428
+ $post_type = get_post_type_object( $this->post_type );
429
+
430
+ if ( $post && ! $this->check_update_permission( $post ) ) {
431
+ return false;
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
454
+ */
455
+ public function delete_item_permissions_check( $request ) {
456
+
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;
464
+ }
465
+
466
+ /**
467
+ * Determine the allowed query_vars for a get_items() response and
468
+ * prepare for WP_Query.
469
+ *
470
+ * @param array $prepared_args
471
+ * @return array $query_args
472
+ */
473
+ protected function prepare_items_query( $prepared_args = array() ) {
474
+
475
+ $valid_vars = array_flip( $this->get_allowed_query_vars() );
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
+
483
+ if ( empty( $query_args['post_status'] ) && 'attachment' === $this->post_type ) {
484
+ $query_args['post_status'] = 'inherit';
485
+ }
486
+
487
+ return $query_args;
488
+ }
489
+
490
+ /**
491
+ * Get all the WP Query vars that are allowed for the API request.
492
+ *
493
+ * @return array
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
+
530
+ return $valid_vars;
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
538
+ */
539
+ protected function prepare_excerpt_response( $excerpt ) {
540
+ if ( post_password_required() ) {
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 ) ) {
547
+ return '';
548
+ }
549
+
550
+ return $excerpt;
551
+ }
552
+
553
+ /**
554
+ * Check the post_date_gmt or modified_gmt and prepare any post or
555
+ * modified date for single post output.
556
+ *
557
+ * @param string $date_gmt
558
+ * @param string|null $date
559
+ * @return string|null ISO8601/RFC3339 formatted datetime.
560
+ */
561
+ protected function prepare_date_response( $date_gmt, $date = null ) {
562
+ if ( '0000-00-00 00:00:00' === $date_gmt ) {
563
+ return null;
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 ) {
574
+ if ( ! empty( $password ) ) {
575
+ /**
576
+ * Fake the correct cookie to fool post_password_required().
577
+ * Without this, get_the_content() will give a password form.
578
+ */
579
+ require_once ABSPATH . 'wp-includes/class-phpass.php';
580
+ $hasher = new PasswordHash( 8, true );
581
+ $value = $hasher->HashPassword( $password );
582
+ $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] = wp_slash( $value );
583
+ }
584
+
585
+ return $password;
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'] );
608
+ } elseif ( ! empty( $request['title']['raw'] ) ) {
609
+ $prepared_post->post_title = wp_filter_post_kses( $request['title']['raw'] );
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'] );
617
+ } elseif ( isset( $request['content']['raw'] ) ) {
618
+ $prepared_post->post_content = wp_filter_post_kses( $request['content']['raw'] );
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'] );
626
+ } elseif ( isset( $request['excerpt']['raw'] ) ) {
627
+ $prepared_post->post_excerpt = wp_filter_post_kses( $request['excerpt']['raw'] );
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.
637
+ $prepared_post->post_type = get_post_type( $request['id'] );
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 ) ) {
645
+ return $status;
646
+ }
647
+
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
675
+ if ( ! empty( $schema['properties']['author'] ) && ! empty( $request['author'] ) ) {
676
+ $author = $this->handle_author_param( $request['author'], $post_type );
677
+ if ( is_wp_error( $author ) ) {
678
+ return $author;
679
+ }
680
+
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'] ) ) {
689
+ return new WP_Error( 'rest_invalid_field', __( 'A post can not be sticky and have a password.' ), array( 'status' => 400 ) );
690
+ }
691
+
692
+ if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) {
693
+ return new WP_Error( 'rest_invalid_field', __( 'A sticky post can not be password protected.' ), array( 'status' => 400 ) );
694
+ }
695
+ }
696
+
697
+ if ( ! empty( $request['sticky'] ) ) {
698
+ if ( ! empty( $prepared_post->ID ) && post_password_required( $prepared_post->ID ) ) {
699
+ return new WP_Error( 'rest_invalid_field', __( 'A password protected post can not be set to sticky.' ), array( 'status' => 400 ) );
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
+ /**
733
+ * Determine validity and normalize provided status param.
734
+ *
735
+ * @param string $post_status
736
+ * @param object $post_type
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':
744
+ case 'pending':
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:
758
+ if ( ! get_post_status_object( $post_status ) ) {
759
+ $post_status = 'draft';
760
+ }
761
+ break;
762
+ }
763
+
764
+ return $post_status;
765
+ }
766
+
767
+ /**
768
+ * Determine validity and normalize provided author param.
769
+ *
770
+ * @param object|integer $post_author
771
+ * @param object $post_type
772
+ * @return WP_Error|integer $post_author
773
+ */
774
+ protected function handle_author_param( $post_author, $post_type ) {
775
+ if ( is_object( $post_author ) ) {
776
+ if ( empty( $post_author->id ) ) {
777
+ return new WP_Error( 'rest_invalid_author', __( 'Invalid author object.' ), array( 'status' => 400 ) );
778
+ }
779
+ $post_author = (int) $post_author->id;
780
+ } else {
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
+
794
+ return $post_author;
795
+ }
796
+
797
+ /**
798
+ * Determine the featured image based on a request param
799
+ *
800
+ * @param int $featured_image
801
+ * @param int $post_id
802
+ */
803
+ protected function handle_featured_image( $featured_image, $post_id ) {
804
+
805
+ $featured_image = (int) $featured_image;
806
+ if ( $featured_image ) {
807
+ $result = set_post_thumbnail( $post_id, $featured_image );
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 );
815
+ }
816
+
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', '' );
830
+ }
831
+ }
832
+
833
+ /**
834
+ * Check if a given post type should be viewed or managed.
835
+ *
836
+ * @param object|string $post_type
837
+ * @return bool Is post type allowed?
838
+ */
839
+ protected function check_is_post_type_allowed( $post_type ) {
840
+ if ( ! is_object( $post_type ) ) {
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
+
848
+ return false;
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 ) {
860
+ if ( ! empty( $post->post_password ) && ! $this->check_update_permission( $post ) ) {
861
+ return false;
862
+ }
863
+
864
+ $post_type = get_post_type_object( $post->post_type );
865
+ if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
866
+ return false;
867
+ }
868
+
869
+ // Can we read the post?
870
+ if ( 'publish' === $post->post_status || current_user_can( $post_type->cap->read_post, $post->ID ) ) {
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 );
877
+
878
+ if ( $this->check_read_permission( $parent ) ) {
879
+ return true;
880
+ }
881
+ }
882
+
883
+ // If we don't have a parent, but the status is set to inherit, assume
884
+ // it's published (as per get_post_status())
885
+ if ( 'inherit' === $post->post_status ) {
886
+ return true;
887
+ }
888
+
889
+ return false;
890
+ }
891
+
892
+ /**
893
+ * Check if we can edit a post
894
+ *
895
+ * @param obj $post Post object
896
+ * @return bool Can we edit it?
897
+ */
898
+ protected function check_update_permission( $post ) {
899
+ $post_type = get_post_type_object( $post->post_type );
900
+
901
+ if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
902
+ return false;
903
+ }
904
+
905
+ return current_user_can( $post_type->cap->edit_post, $post->ID );
906
+ }
907
+
908
+ /**
909
+ * Check if we can create a post
910
+ *
911
+ * @param obj $post Post object
912
+ * @return bool Can we create it?
913
+ */
914
+ protected function check_create_permission( $post ) {
915
+ $post_type = get_post_type_object( $post->post_type );
916
+
917
+ if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
918
+ return false;
919
+ }
920
+
921
+ return current_user_can( $post_type->cap->create_posts );
922
+ }
923
+
924
+ /**
925
+ * Check if we can delete a post
926
+ *
927
+ * @param obj $post Post object
928
+ * @return bool Can we delete it?
929
+ */
930
+ protected function check_delete_permission( $post ) {
931
+ $post_type = get_post_type_object( $post->post_type );
932
+
933
+ if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
934
+ return false;
935
+ }
936
+
937
+ return current_user_can( $post_type->cap->delete_post, $post->ID );
938
+ }
939
+
940
+ /**
941
+ * Get the base path for a post type's endpoints.
942
+ *
943
+ * @param object|string $post_type
944
+ * @return string $base
945
+ */
946
+ public function get_post_type_base( $post_type ) {
947
+ if ( ! is_object( $post_type ) ) {
948
+ $post_type = get_post_type_object( $post_type );
949
+ }
950
+
951
+ $base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
952
+
953
+ return $base;
954
+ }
955
+
956
+ /**
957
+ * Prepare a single post output for response
958
+ *
959
+ * @param WP_Post $post Post object
960
+ * @param WP_REST_Request $request Request object
961
+ * @return WP_REST_Response $data
962
+ */
963
+ public function prepare_item_for_response( $post, $request ) {
964
+ $GLOBALS['post'] = $post;
965
+ setup_postdata( $post );
966
+
967
+ // Base fields for every post
968
+ $data = array(
969
+ 'id' => $post->ID,
970
+ 'date' => $this->prepare_date_response( $post->post_date_gmt, $post->post_date ),
971
+ 'date_gmt' => $this->prepare_date_response( $post->post_date_gmt ),
972
+ 'guid' => array(
973
+ 'rendered' => apply_filters( 'get_the_guid', $post->guid ),
974
+ 'raw' => $post->guid,
975
+ ),
976
+ 'modified' => $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified ),
977
+ 'modified_gmt' => $this->prepare_date_response( $post->post_modified_gmt ),
978
+ 'password' => $post->post_password,
979
+ 'slug' => $post->post_name,
980
+ 'status' => $post->post_status,
981
+ 'type' => $post->post_type,
982
+ 'link' => get_permalink( $post->ID ),
983
+ );
984
+
985
+ $schema = $this->get_item_schema();
986
+
987
+ if ( ! empty( $schema['properties']['title'] ) ) {
988
+ $data['title'] = array(
989
+ 'raw' => $post->post_title,
990
+ 'rendered' => get_the_title( $post->ID ),
991
+ );
992
+ }
993
+
994
+ if ( ! empty( $schema['properties']['content'] ) ) {
995
+
996
+ if ( ! empty( $post->post_password ) ) {
997
+ $this->prepare_password_response( $post->post_password );
998
+ }
999
+
1000
+ $data['content'] = array(
1001
+ 'raw' => $post->post_content,
1002
+ 'rendered' => apply_filters( 'the_content', $post->post_content ),
1003
+ );
1004
+
1005
+ // Don't leave our cookie lying around: https://github.com/WP-API/WP-API/issues/1055
1006
+ if ( ! empty( $post->post_password ) ) {
1007
+ $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] = '';
1008
+ }
1009
+ }
1010
+
1011
+ if ( ! empty( $schema['properties']['excerpt'] ) ) {
1012
+ $data['excerpt'] = array(
1013
+ 'raw' => $post->post_excerpt,
1014
+ 'rendered' => $this->prepare_excerpt_response( $post->post_excerpt ),
1015
+ );
1016
+ }
1017
+
1018
+ if ( ! empty( $schema['properties']['author'] ) ) {
1019
+ $data['author'] = (int) $post->post_author;
1020
+ }
1021
+
1022
+ if ( ! empty( $schema['properties']['featured_image'] ) ) {
1023
+ $data['featured_image'] = (int) get_post_thumbnail_id( $post->ID );
1024
+ }
1025
+
1026
+ if ( ! empty( $schema['properties']['parent'] ) ) {
1027
+ $data['parent'] = (int) $post->post_parent;
1028
+ }
1029
+
1030
+ if ( ! empty( $schema['properties']['menu_order'] ) ) {
1031
+ $data['menu_order'] = (int) $post->menu_order;
1032
+ }
1033
+
1034
+ if ( ! empty( $schema['properties']['comment_status'] ) ) {
1035
+ $data['comment_status'] = $post->comment_status;
1036
+ }
1037
+
1038
+ if ( ! empty( $schema['properties']['ping_status'] ) ) {
1039
+ $data['ping_status'] = $post->ping_status;
1040
+ }
1041
+
1042
+ if ( ! empty( $schema['properties']['sticky'] ) ) {
1043
+ $data['sticky'] = is_sticky( $post->ID );
1044
+ }
1045
+
1046
+ if ( ! empty( $schema['properties']['template'] ) ) {
1047
+ if ( $template = get_page_template_slug( $post->ID ) ) {
1048
+ $data['template'] = $template;
1049
+ } else {
1050
+ $data['template'] = '';
1051
+ }
1052
+ }
1053
+
1054
+ if ( ! empty( $schema['properties']['format'] ) ) {
1055
+ $data['format'] = get_post_format( $post->ID );
1056
+ // Fill in blank post format
1057
+ if ( empty( $data['format'] ) ) {
1058
+ $data['format'] = 'standard';
1059
+ }
1060
+ }
1061
+
1062
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
1063
+ $data = $this->filter_response_by_context( $data, $context );
1064
+
1065
+ $data = $this->add_additional_fields_to_object( $data, $request );
1066
+
1067
+ // Wrap the data in a response object
1068
+ $data = rest_ensure_response( $data );
1069
+
1070
+ $data->add_links( $this->prepare_links( $post ) );
1071
+
1072
+ return apply_filters( 'rest_prepare_' . $this->post_type, $data, $post, $request );
1073
+ }
1074
+
1075
+ /**
1076
+ * Prepare links for the request.
1077
+ *
1078
+ * @param WP_Post $post Post object.
1079
+ * @return array Links for the given post.
1080
+ */
1081
+ protected function prepare_links( $post ) {
1082
+ $base = '/wp/v2/' . $this->get_post_type_base( $this->post_type );
1083
+
1084
+ // Entity meta
1085
+ $links = array(
1086
+ 'self' => array(
1087
+ 'href' => rest_url( trailingslashit( $base ) . $post->ID ),
1088
+ ),
1089
+ 'collection' => array(
1090
+ 'href' => rest_url( $base ),
1091
+ ),
1092
+ );
1093
+
1094
+ if ( ( in_array( $post->post_type, array( 'post', 'page' ) ) || post_type_supports( $post->post_type, 'author' ) )
1095
+ && ! empty( $post->post_author ) ) {
1096
+ $links['author'] = array(
1097
+ 'href' => rest_url( '/wp/v2/users/' . $post->post_author ),
1098
+ 'embeddable' => true,
1099
+ );
1100
+ };
1101
+
1102
+ if ( in_array( $post->post_type, array( 'post', 'page' ) ) || post_type_supports( $post->post_type, 'comments' ) ) {
1103
+ $replies_url = rest_url( '/wp/v2/comments' );
1104
+ $replies_url = add_query_arg( 'post_id', $post->ID, $replies_url );
1105
+ $links['replies'] = array(
1106
+ 'href' => $replies_url,
1107
+ 'embeddable' => true,
1108
+ );
1109
+ }
1110
+
1111
+ if ( in_array( $post->post_type, array( 'post', 'page' ) ) || post_type_supports( $post->post_type, 'revisions' ) ) {
1112
+ $links['version-history'] = array(
1113
+ 'href' => rest_url( trailingslashit( $base ) . $post->ID . '/revisions' ),
1114
+ );
1115
+ }
1116
+ $post_type_obj = get_post_type_object( $post->post_type );
1117
+ if ( $post_type_obj->hierarchical && ! empty( $post->post_parent ) ) {
1118
+ $links['up'] = array(
1119
+ 'href' => rest_url( trailingslashit( $base ) . (int) $post->post_parent ),
1120
+ 'embeddable' => true,
1121
+ );
1122
+ }
1123
+
1124
+ if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ) ) ) {
1125
+ $attachments_url = rest_url( 'wp/v2/media' );
1126
+ $attachments_url = add_query_arg( 'post_parent', $post->ID, $attachments_url );
1127
+ $links['http://v2.wp-api.org/attachment'] = array(
1128
+ 'href' => $attachments_url,
1129
+ 'embeddable' => true,
1130
+ );
1131
+ }
1132
+
1133
+ $taxonomies = get_object_taxonomies( $post->post_type );
1134
+ if ( ! empty( $taxonomies ) ) {
1135
+ $links['http://v2.wp-api.org/term'] = array();
1136
+
1137
+ foreach ( $taxonomies as $tax ) {
1138
+ $taxonomy_obj = get_taxonomy( $tax );
1139
+ // Skip taxonomies that are not public.
1140
+ if ( false === $taxonomy_obj->public ) {
1141
+ continue;
1142
+ }
1143
+
1144
+ if ( 'post_tag' === $tax ) {
1145
+ $terms_url = rest_url( '/wp/v2/terms/tag' );
1146
+ } else {
1147
+ $terms_url = rest_url( '/wp/v2/terms/' . $tax );
1148
+ }
1149
+
1150
+ $terms_url = add_query_arg( 'post', $post->ID, $terms_url );
1151
+
1152
+ $links['http://v2.wp-api.org/term'][] = array(
1153
+ 'href' => $terms_url,
1154
+ 'taxonomy' => $tax,
1155
+ 'embeddable' => true,
1156
+ );
1157
+ }
1158
+ }
1159
+
1160
+ if ( post_type_supports( $post->post_type, 'custom-fields' ) ) {
1161
+ $links['http://v2.wp-api.org/meta'] = array(
1162
+ 'href' => rest_url( trailingslashit( $base ) . $post->ID . '/meta' ),
1163
+ 'embeddable' => true,
1164
+ );
1165
+ }
1166
+
1167
+ return $links;
1168
+ }
1169
+
1170
+ /**
1171
+ * Get the Post's schema, conforming to JSON Schema
1172
+ *
1173
+ * @return array
1174
+ */
1175
+ public function get_item_schema() {
1176
+
1177
+ $base = $this->get_post_type_base( $this->post_type );
1178
+ $schema = array(
1179
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
1180
+ 'title' => $this->post_type,
1181
+ 'type' => 'object',
1182
+ /*
1183
+ * Base properties for every Post
1184
+ */
1185
+ 'properties' => array(
1186
+ 'date' => array(
1187
+ 'description' => 'The date the object was published.',
1188
+ 'type' => 'string',
1189
+ 'format' => 'date-time',
1190
+ 'context' => array( 'view', 'edit', 'embed' ),
1191
+ ),
1192
+ 'date_gmt' => array(
1193
+ 'description' => 'The date the object was published, as GMT.',
1194
+ 'type' => 'string',
1195
+ 'format' => 'date-time',
1196
+ 'context' => array( 'edit' ),
1197
+ ),
1198
+ 'guid' => array(
1199
+ 'description' => 'The globally unique identifier for the object.',
1200
+ 'type' => 'object',
1201
+ 'context' => array( 'view', 'edit' ),
1202
+ 'readonly' => true,
1203
+ 'properties' => array(
1204
+ 'raw' => array(
1205
+ 'description' => 'GUID for the object, as it exists in the database.',
1206
+ 'type' => 'string',
1207
+ 'context' => array( 'edit' ),
1208
+ ),
1209
+ 'rendered' => array(
1210
+ 'description' => 'GUID for the object, transformed for display.',
1211
+ 'type' => 'string',
1212
+ 'context' => array( 'view', 'edit' ),
1213
+ ),
1214
+ ),
1215
+ ),
1216
+ 'id' => array(
1217
+ 'description' => 'Unique identifier for the object.',
1218
+ 'type' => 'integer',
1219
+ 'context' => array( 'view', 'edit', 'embed' ),
1220
+ 'readonly' => true,
1221
+ ),
1222
+ 'link' => array(
1223
+ 'description' => 'URL to the object.',
1224
+ 'type' => 'string',
1225
+ 'format' => 'uri',
1226
+ 'context' => array( 'view', 'edit', 'embed' ),
1227
+ 'readonly' => true,
1228
+ ),
1229
+ 'modified' => array(
1230
+ 'description' => 'The date the object was last modified.',
1231
+ 'type' => 'string',
1232
+ 'format' => 'date-time',
1233
+ 'context' => array( 'view', 'edit' ),
1234
+ ),
1235
+ 'modified_gmt' => array(
1236
+ 'description' => 'The date the object was last modified, as GMT.',
1237
+ 'type' => 'string',
1238
+ 'format' => 'date-time',
1239
+ 'context' => array( 'view', 'edit' ),
1240
+ ),
1241
+ 'password' => array(
1242
+ 'description' => 'A password to protect access to the post.',
1243
+ 'type' => 'string',
1244
+ 'context' => array( 'edit' ),
1245
+ ),
1246
+ 'slug' => array(
1247
+ 'description' => 'An alphanumeric identifier for the object unique to its type.',
1248
+ 'type' => 'string',
1249
+ 'context' => array( 'view', 'edit', 'embed' ),
1250
+ ),
1251
+ 'status' => array(
1252
+ 'description' => 'A named status for the object.',
1253
+ 'type' => 'string',
1254
+ 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
1255
+ 'context' => array( 'edit' ),
1256
+ ),
1257
+ 'type' => array(
1258
+ 'description' => 'Type of Post for the object.',
1259
+ 'type' => 'string',
1260
+ 'context' => array( 'view', 'edit', 'embed' ),
1261
+ 'readonly' => true,
1262
+ ),
1263
+ ),
1264
+ );
1265
+
1266
+ $post_type_obj = get_post_type_object( $this->post_type );
1267
+ if ( $post_type_obj->hierarchical ) {
1268
+ $schema['properties']['parent'] = array(
1269
+ 'description' => 'The ID for the parent of the object.',
1270
+ 'type' => 'integer',
1271
+ 'context' => array( 'view', 'edit' ),
1272
+ );
1273
+ }
1274
+
1275
+ $post_type_attributes = array(
1276
+ 'title',
1277
+ 'editor',
1278
+ 'author',
1279
+ 'excerpt',
1280
+ 'thumbnail',
1281
+ 'comments',
1282
+ 'revisions',
1283
+ 'page-attributes',
1284
+ 'post-formats',
1285
+ );
1286
+ $fixed_schemas = array(
1287
+ 'post' => array(
1288
+ 'title',
1289
+ 'editor',
1290
+ 'author',
1291
+ 'excerpt',
1292
+ 'thumbnail',
1293
+ 'comments',
1294
+ 'revisions',
1295
+ 'post-formats',
1296
+ ),
1297
+ 'page' => array(
1298
+ 'title',
1299
+ 'editor',
1300
+ 'author',
1301
+ 'excerpt',
1302
+ 'thumbnail',
1303
+ 'comments',
1304
+ 'revisions',
1305
+ 'page-attributes',
1306
+ ),
1307
+ 'attachment' => array(
1308
+ 'title',
1309
+ 'author',
1310
+ 'comments',
1311
+ 'revisions',
1312
+ ),
1313
+ );
1314
+ foreach ( $post_type_attributes as $attribute ) {
1315
+ if ( isset( $fixed_schemas[ $this->post_type ] ) && ! in_array( $attribute, $fixed_schemas[ $this->post_type ] ) ) {
1316
+ continue;
1317
+ } elseif ( ! in_array( $this->post_type, array_keys( $fixed_schemas ) ) && ! post_type_supports( $this->post_type, $attribute ) ) {
1318
+ continue;
1319
+ }
1320
+
1321
+ switch ( $attribute ) {
1322
+
1323
+ case 'title':
1324
+ $schema['properties']['title'] = array(
1325
+ 'description' => 'The title for the object.',
1326
+ 'type' => 'object',
1327
+ 'context' => array( 'view', 'edit', 'embed' ),
1328
+ 'properties' => array(
1329
+ 'raw' => array(
1330
+ 'description' => 'Title for the object, as it exists in the database.',
1331
+ 'type' => 'string',
1332
+ 'context' => array( 'edit' ),
1333
+ ),
1334
+ 'rendered' => array(
1335
+ 'description' => 'Title for the object, transformed for display.',
1336
+ 'type' => 'string',
1337
+ 'context' => array( 'view', 'edit', 'embed' ),
1338
+ ),
1339
+ ),
1340
+ );
1341
+ break;
1342
+
1343
+ case 'editor':
1344
+ $schema['properties']['content'] = array(
1345
+ 'description' => 'The content for the object.',
1346
+ 'type' => 'object',
1347
+ 'context' => array( 'view', 'edit' ),
1348
+ 'properties' => array(
1349
+ 'raw' => array(
1350
+ 'description' => 'Content for the object, as it exists in the database.',
1351
+ 'type' => 'string',
1352
+ 'context' => array( 'edit' ),
1353
+ ),
1354
+ 'rendered' => array(
1355
+ 'description' => 'Content for the object, transformed for display.',
1356
+ 'type' => 'string',
1357
+ 'context' => array( 'view', 'edit' ),
1358
+ ),
1359
+ ),
1360
+ );
1361
+ break;
1362
+
1363
+ case 'author':
1364
+ $schema['properties']['author'] = array(
1365
+ 'description' => 'The ID for the author of the object.',
1366
+ 'type' => 'integer',
1367
+ 'context' => array( 'view', 'edit', 'embed' ),
1368
+ );
1369
+ break;
1370
+
1371
+ case 'excerpt':
1372
+ $schema['properties']['excerpt'] = array(
1373
+ 'description' => 'The excerpt for the object.',
1374
+ 'type' => 'object',
1375
+ 'context' => array( 'view', 'edit', 'embed' ),
1376
+ 'properties' => array(
1377
+ 'raw' => array(
1378
+ 'description' => 'Excerpt for the object, as it exists in the database.',
1379
+ 'type' => 'string',
1380
+ 'context' => array( 'edit' ),
1381
+ ),
1382
+ 'rendered' => array(
1383
+ 'description' => 'Excerpt for the object, transformed for display.',
1384
+ 'type' => 'string',
1385
+ 'context' => array( 'view', 'edit', 'embed' ),
1386
+ ),
1387
+ ),
1388
+ );
1389
+ break;
1390
+
1391
+ case 'thumbnail':
1392
+ $schema['properties']['featured_image'] = array(
1393
+ 'description' => 'ID of the featured image for the object.',
1394
+ 'type' => 'integer',
1395
+ 'context' => array( 'view', 'edit' ),
1396
+ );
1397
+ break;
1398
+
1399
+ case 'comments':
1400
+ $schema['properties']['comment_status'] = array(
1401
+ 'description' => 'Whether or not comments are open on the object.',
1402
+ 'type' => 'string',
1403
+ 'enum' => array( 'open', 'closed' ),
1404
+ 'context' => array( 'view', 'edit' ),
1405
+ );
1406
+ $schema['properties']['ping_status'] = array(
1407
+ 'description' => 'Whether or not the object can be pinged.',
1408
+ 'type' => 'string',
1409
+ 'enum' => array( 'open', 'closed' ),
1410
+ 'context' => array( 'view', 'edit' ),
1411
+ );
1412
+ break;
1413
+
1414
+ case 'page-attributes':
1415
+ $schema['properties']['menu_order'] = array(
1416
+ 'description' => 'The order of the object in relation to other object of its type.',
1417
+ 'type' => 'integer',
1418
+ 'context' => array( 'view', 'edit' ),
1419
+ );
1420
+ break;
1421
+
1422
+ case 'post-formats':
1423
+ $schema['properties']['format'] = array(
1424
+ 'description' => 'The format for the object.',
1425
+ 'type' => 'string',
1426
+ 'enum' => get_post_format_slugs(),
1427
+ 'context' => array( 'view', 'edit' ),
1428
+ );
1429
+ break;
1430
+
1431
+ }
1432
+ }
1433
+
1434
+ if ( 'post' === $this->post_type ) {
1435
+ $schema['properties']['sticky'] = array(
1436
+ 'description' => 'Whether or not the object should be treated as sticky.',
1437
+ 'type' => 'boolean',
1438
+ 'context' => array( 'view', 'edit' ),
1439
+ );
1440
+ }
1441
+
1442
+ if ( 'page' === $this->post_type ) {
1443
+ $schema['properties']['template'] = array(
1444
+ 'description' => 'The theme file to use to display the object.',
1445
+ 'type' => 'string',
1446
+ 'enum' => array_values( get_page_templates() ),
1447
+ 'context' => array( 'view', 'edit' ),
1448
+ );
1449
+ }
1450
+
1451
+ return $this->add_additional_fields_schema( $schema );
1452
+ }
1453
+
1454
+ }
lib/endpoints/class-wp-rest-posts-terms-controller.php ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_REST_Posts_Terms_Controller extends WP_REST_Controller {
4
+
5
+ protected $post_type;
6
+
7
+ public function __construct( $post_type, $taxonomy ) {
8
+ $this->post_type = $post_type;
9
+ $this->taxonomy = $taxonomy;
10
+ $this->posts_controller = new WP_REST_Posts_Controller( $post_type );
11
+ $this->terms_controller = new WP_REST_Terms_Controller( $taxonomy );
12
+ }
13
+
14
+ /**
15
+ * Register the routes for the objects of the 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
+
55
+ /**
56
+ * Get all the terms that are attached to a post
57
+ *
58
+ * @param WP_REST_Request $request Full details about the request
59
+ * @return WP_Error|WP_REST_Response
60
+ */
61
+ public function get_items( $request ) {
62
+
63
+ $post = get_post( absint( $request['post_id'] ) );
64
+
65
+ $is_request_valid = $this->validate_request( $request );
66
+ if ( is_wp_error( $is_request_valid ) ) {
67
+ return $is_request_valid;
68
+ }
69
+
70
+ $args = array(
71
+ 'order' => $request['order'],
72
+ 'orderby' => $request['orderby'],
73
+ );
74
+ $terms = wp_get_object_terms( $post->ID, $this->taxonomy, $args );
75
+
76
+ $response = array();
77
+ foreach ( $terms as $term ) {
78
+ $data = $this->terms_controller->prepare_item_for_response( $term, $request );
79
+ $response[] = $this->prepare_response_for_collection( $data );
80
+ }
81
+
82
+ $response = rest_ensure_response( $response );
83
+
84
+ return $response;
85
+ }
86
+
87
+ /**
88
+ * Get a term that is attached to a post
89
+ *
90
+ * @param WP_REST_Request $request Full details about the request
91
+ * @return WP_Error|WP_REST_Response
92
+ */
93
+ public function get_item( $request ) {
94
+ $post = get_post( absint( $request['post_id'] ) );
95
+ $term_id = absint( $request['term_id'] );
96
+
97
+ $is_request_valid = $this->validate_request( $request );
98
+ if ( is_wp_error( $is_request_valid ) ) {
99
+ return $is_request_valid;
100
+ }
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
+
112
+ return $response;
113
+ }
114
+
115
+ /**
116
+ * Add a term to a post
117
+ *
118
+ * @param WP_REST_Request $request Full details about the request
119
+ * @return WP_Error|WP_REST_Response
120
+ */
121
+ public function create_item( $request ) {
122
+ $post = get_post( $request['post_id'] );
123
+ $term_id = absint( $request['term_id'] );
124
+
125
+ $is_request_valid = $this->validate_request( $request );
126
+ if ( is_wp_error( $is_request_valid ) ) {
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
+
144
+ /**
145
+ * Remove a term from a post.
146
+ *
147
+ * @param WP_REST_Request $request Full details about the request
148
+ * @return WP_Error|null
149
+ */
150
+ public function delete_item( $request ) {
151
+ $post = get_post( absint( $request['post_id'] ) );
152
+ $term_id = absint( $request['term_id'] );
153
+ $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
154
+
155
+ // We don't support trashing for this type, error out
156
+ if ( ! $force ) {
157
+ return new WP_Error( 'rest_trash_not_supported', __( 'Terms do not support trashing.' ), array( 'status' => 501 ) );
158
+ }
159
+
160
+ $is_request_valid = $this->validate_request( $request );
161
+ if ( is_wp_error( $is_request_valid ) ) {
162
+ return $is_request_valid;
163
+ }
164
+
165
+ $previous_item = $this->get_item( $request );
166
+
167
+ $remove = wp_remove_object_terms( $post->ID, $term_id, $this->taxonomy );
168
+
169
+ if ( is_wp_error( $remove ) ) {
170
+ return $remove;
171
+ }
172
+
173
+ return $previous_item;
174
+ }
175
+
176
+ /**
177
+ * Get the Term schema, conforming to JSON Schema.
178
+ *
179
+ * @return array
180
+ */
181
+ public function get_item_schema() {
182
+ return $this->terms_controller->get_item_schema();
183
+ }
184
+
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
+ }
208
+
209
+ return true;
210
+ }
211
+
212
+ /**
213
+ * Check if a given request has access to read a post's term.
214
+ *
215
+ * @param WP_REST_Request $request Full details about the request.
216
+ * @return bool|WP_Error
217
+ */
218
+ public function get_items_permissions_check( $request ) {
219
+
220
+ $post_request = new WP_REST_Request();
221
+ $post_request->set_param( 'id', $request['post_id'] );
222
+
223
+ $post_check = $this->posts_controller->get_item_permissions_check( $post_request );
224
+
225
+ if ( ! $post_check || is_wp_error( $post_check ) ) {
226
+ return $post_check;
227
+ }
228
+
229
+ $term_request = new WP_REST_Request();
230
+ $term_request->set_param( 'id', $request['term_id'] );
231
+
232
+ $terms_check = $this->terms_controller->get_item_permissions_check( $term_request );
233
+
234
+ if ( ! $terms_check || is_wp_error( $terms_check ) ) {
235
+ return $terms_check;
236
+ }
237
+
238
+ return true;
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'] );
251
+ $post_check = $this->posts_controller->update_item_permissions_check( $post_request );
252
+
253
+ if ( ! $post_check || is_wp_error( $post_check ) ) {
254
+ return $post_check;
255
+ }
256
+
257
+ return true;
258
+ }
259
+
260
+ /**
261
+ * Get the query params for collections
262
+ *
263
+ * @return array
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(
278
+ 'count',
279
+ 'name',
280
+ 'slug',
281
+ 'term_order',
282
+ ),
283
+ );
284
+ return $query_params;
285
+ }
286
+
287
+ }
lib/endpoints/class-wp-rest-revisions-controller.php ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_REST_Revisions_Controller extends WP_REST_Controller {
4
+
5
+ private $parent_post_type;
6
+ private $parent_controller;
7
+ private $parent_base;
8
+
9
+ public function __construct( $parent_post_type ) {
10
+ $this->parent_post_type = $parent_post_type;
11
+ $this->parent_controller = new WP_REST_Posts_Controller( $parent_post_type );
12
+ $this->parent_base = $this->parent_controller->get_post_type_base( $this->parent_post_type );
13
+ }
14
+
15
+ /**
16
+ * Register routes for revisions based on post types supporting revisions
17
+ */
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(
32
+ array(
33
+ 'methods' => WP_REST_Server::READABLE,
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(
43
+ 'methods' => WP_REST_Server::DELETABLE,
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
+
56
+ /**
57
+ * Get a collection of revisions
58
+ *
59
+ * @param WP_REST_Request $request Full data about the request.
60
+ * @return WP_Error|WP_REST_Response
61
+ */
62
+ public function get_items( $request ) {
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
+ /**
79
+ * Check if a given request has access to get revisions
80
+ *
81
+ * @param WP_REST_Request $request Full data about the request.
82
+ * @return WP_Error|bool
83
+ */
84
+ public function get_items_permissions_check( $request ) {
85
+
86
+ $parent = get_post( $request['parent_id'] );
87
+ if ( ! $parent ) {
88
+ return true;
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;
96
+ }
97
+
98
+ /**
99
+ * Get one revision from the collection
100
+ *
101
+ * @param WP_REST_Request $request Full data about the request.
102
+ * @return WP_Error|array
103
+ */
104
+ public function get_item( $request ) {
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 );
117
+ return $response;
118
+ }
119
+
120
+ /**
121
+ * Check if a given request has access to get a specific revision
122
+ *
123
+ * @param WP_REST_Request $request Full data about the request.
124
+ * @return WP_Error|bool
125
+ */
126
+ public function get_item_permissions_check( $request ) {
127
+ return $this->get_items_permissions_check( $request );
128
+ }
129
+
130
+ /**
131
+ * Delete a single revision
132
+ *
133
+ * @param WP_REST_Request $request Full details about the request
134
+ * @return bool|WP_Error
135
+ */
136
+ public function delete_item( $request ) {
137
+ $result = wp_delete_post( $request['id'], true );
138
+ if ( $result ) {
139
+ return true;
140
+ } else {
141
+ return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Check if a given request has access to delete a revision
147
+ *
148
+ * @param WP_REST_Request $request Full details about the request.
149
+ * @return bool|WP_Error
150
+ */
151
+ public function delete_item_permissions_check( $request ) {
152
+
153
+ $response = $this->get_items_permissions_check( $request );
154
+ if ( ! $response || is_wp_error( $response ) ) {
155
+ return $response;
156
+ }
157
+
158
+ $post = get_post( $request['id'] );
159
+ $post_type = get_post_type_object( 'revision' );
160
+ return current_user_can( $post_type->cap->delete_post, $post->ID );
161
+ }
162
+
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
+ */
170
+ public function prepare_item_for_response( $post, $request ) {
171
+
172
+ // Base fields for every post
173
+ $data = array(
174
+ 'author' => $post->post_author,
175
+ 'date' => $this->prepare_date_response( $post->post_date_gmt, $post->post_date ),
176
+ 'date_gmt' => $this->prepare_date_response( $post->post_date_gmt ),
177
+ 'guid' => $post->guid,
178
+ 'id' => $post->ID,
179
+ 'modified' => $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified ),
180
+ 'modified_gmt' => $this->prepare_date_response( $post->post_modified_gmt ),
181
+ 'parent' => (int) $post->post_parent,
182
+ 'slug' => $post->post_name,
183
+ );
184
+
185
+ $schema = $this->get_item_schema();
186
+
187
+ if ( ! empty( $schema['properties']['title'] ) ) {
188
+ $data['title'] = $post->post_title;
189
+ }
190
+
191
+ if ( ! empty( $schema['properties']['content'] ) ) {
192
+ $data['content'] = $post->post_content;
193
+ }
194
+
195
+ if ( ! empty( $schema['properties']['excerpt'] ) ) {
196
+ $data['excerpt'] = $post->post_excerpt;
197
+ }
198
+
199
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
200
+ $data = $this->filter_response_by_context( $data, $context );
201
+ $data = $this->add_additional_fields_to_object( $data, $request );
202
+ $response = rest_ensure_response( $data );
203
+ if ( is_wp_error( $response ) ) {
204
+ return $response;
205
+ }
206
+
207
+ if ( ! empty( $data['parent'] ) ) {
208
+ $response->add_link( 'parent', rest_url( sprintf( 'wp/%s/%d', $this->parent_base, $data['parent'] ) ) );
209
+ }
210
+
211
+ return $response;
212
+ }
213
+
214
+ /**
215
+ * Check the post_date_gmt or modified_gmt and prepare any post or
216
+ * modified date for single post output.
217
+ *
218
+ * @param string $date_gmt
219
+ * @param string|null $date
220
+ * @return string|null ISO8601/RFC3339 formatted datetime.
221
+ */
222
+ protected function prepare_date_response( $date_gmt, $date = null ) {
223
+ if ( '0000-00-00 00:00:00' === $date_gmt ) {
224
+ return null;
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
+ /**
235
+ * Get the revision's schema, conforming to JSON Schema
236
+ *
237
+ * @return array
238
+ */
239
+ public function get_item_schema() {
240
+ $schema = array(
241
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
242
+ 'title' => "{$this->parent_base}-revision",
243
+ 'type' => 'object',
244
+ /*
245
+ * Base properties for every Revision
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
+ ),
297
+ ),
298
+ );
299
+
300
+ $parent_schema = $this->parent_controller->get_item_schema();
301
+
302
+ foreach ( array( 'title', 'content', 'excerpt' ) as $property ) {
303
+ if ( empty( $parent_schema['properties'][ $property ] ) ) {
304
+ continue;
305
+ }
306
+
307
+ switch ( $property ) {
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
+ );
315
+ break;
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
+ );
323
+ break;
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
+ );
331
+ break;
332
+
333
+ }
334
+ }
335
+
336
+ return $this->add_additional_fields_schema( $schema );
337
+ }
338
+
339
+ }
lib/endpoints/class-wp-rest-taxonomies-controller.php ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WP_REST_Taxonomies_Controller extends WP_REST_Controller {
4
+
5
+ /**
6
+ * Register the routes for the objects of the controller.
7
+ */
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
+
30
+ /**
31
+ * Get all public taxonomies
32
+ *
33
+ * @param WP_REST_Request $request
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
+ }
52
+
53
+ /**
54
+ * Get a specific taxonomy
55
+ *
56
+ * @param WP_REST_Request $request
57
+ * @return array|WP_Error
58
+ */
59
+ public function get_item( $request ) {
60
+ $tax_obj = get_taxonomy( $request['taxonomy'] );
61
+ if ( empty( $tax_obj ) ) {
62
+ return new WP_Error( 'rest_taxonomy_invalid', __( 'Invalid taxonomy.' ), array( 'status' => 404 ) );
63
+ }
64
+ return $this->prepare_item_for_response( $tax_obj, $request );
65
+ }
66
+
67
+ /**
68
+ * Check if a given request has access a taxonomy
69
+ *
70
+ * @param WP_REST_Request $request Full details about the request.
71
+ * @return bool
72
+ */
73
+ public function get_item_permissions_check( $request ) {
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;
82
+ }
83
+
84
+ /**
85
+ * Prepare a taxonomy object for serialization
86
+ *
87
+ * @param stdClass $taxonomy Taxonomy data
88
+ * @param WP_REST_Request $request
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,
98
+ 'slug' => $taxonomy->name,
99
+ 'description' => $taxonomy->description,
100
+ 'labels' => $taxonomy->labels,
101
+ 'types' => $taxonomy->object_type,
102
+ 'show_cloud' => $taxonomy->show_tagcloud,
103
+ 'hierarchical' => $taxonomy->hierarchical,
104
+ );
105
+
106
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
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
+ /**
114
+ * Get the taxonomy's schema, conforming to JSON Schema
115
+ *
116
+ * @return array
117
+ */
118
+ public function get_item_schema() {
119
+ $schema = array(
120
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
121
+ 'title' => 'taxonomy',
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
+ }
lib/endpoints/class-wp-rest-terms-controller.php ADDED
@@ -0,0 +1,595 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Access terms associated with a taxonomy
5
+ */
6
+ class WP_REST_Terms_Controller extends WP_REST_Controller {
7
+
8
+ protected $taxonomy;
9
+
10
+ /**
11
+ * @param string $taxonomy
12
+ */
13
+ public function __construct( $taxonomy ) {
14
+ $this->taxonomy = $taxonomy;
15
+ }
16
+
17
+ /**
18
+ * Register the routes for the objects of the controller.
19
+ */
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
+
85
+ /**
86
+ * Get terms associated with a taxonomy
87
+ *
88
+ * @param WP_REST_Request $request Full details about the request
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 ) {
111
+ $data = $this->prepare_item_for_response( $term, $request );
112
+ $response[] = $this->prepare_response_for_collection( $data );
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
+ }
137
+
138
+ return $response;
139
+ }
140
+
141
+ /**
142
+ * Get a single term from a taxonomy
143
+ *
144
+ * @param WP_REST_Request $request Full details about the request
145
+ * @return WP_REST_Request|WP_Error
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 ) ) {
154
+ return $term;
155
+ }
156
+
157
+ $response = $this->prepare_item_for_response( $term, $request );
158
+
159
+ return rest_ensure_response( $response );
160
+ }
161
+
162
+ /**
163
+ * Create a single term for a taxonomy
164
+ *
165
+ * @param WP_REST_Request $request Full details about the request
166
+ * @return WP_REST_Request|WP_Error
167
+ */
168
+ public function create_item( $request ) {
169
+ $name = $request['name'];
170
+
171
+ $args = array();
172
+
173
+ if ( isset( $request['description'] ) ) {
174
+ $args['description'] = $request['description'];
175
+ }
176
+ if ( isset( $request['slug'] ) ) {
177
+ $args['slug'] = $request['slug'];
178
+ }
179
+
180
+ if ( isset( $request['parent'] ) ) {
181
+ if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
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 ) );
189
+ }
190
+
191
+ $args['parent'] = $parent->term_id;
192
+ }
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
+ /**
209
+ * Update a single term from a taxonomy
210
+ *
211
+ * @param WP_REST_Request $request Full details about the request
212
+ * @return WP_REST_Request|WP_Error
213
+ */
214
+ public function update_item( $request ) {
215
+
216
+ $prepared_args = array();
217
+ if ( isset( $request['name'] ) ) {
218
+ $prepared_args['name'] = $request['name'];
219
+ }
220
+ if ( isset( $request['description'] ) ) {
221
+ $prepared_args['description'] = $request['description'];
222
+ }
223
+ if ( isset( $request['slug'] ) ) {
224
+ $prepared_args['slug'] = $request['slug'];
225
+ }
226
+
227
+ if ( isset( $request['parent'] ) ) {
228
+ if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
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 ) );
236
+ }
237
+
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 ) ) {
248
+ $update = wp_update_term( $term->term_id, $term->taxonomy, $prepared_args );
249
+ if ( is_wp_error( $update ) ) {
250
+ return $update;
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
+ }
262
+
263
+ /**
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 ) );
280
+ }
281
+
282
+ return $response;
283
+ }
284
+
285
+ /**
286
+ * Check if a given request has access to read the terms.
287
+ *
288
+ * @param WP_REST_Request $request Full details about the request.
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
+ /**
307
+ * Check if a given request has access to read a term.
308
+ *
309
+ * @param WP_REST_Request $request Full details about the request.
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
+
328
+ /**
329
+ * Check if a given request has access to create a term
330
+ *
331
+ * @param WP_REST_Request $request Full details about the request.
332
+ * @return bool|WP_Error
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;
347
+ }
348
+
349
+ /**
350
+ * Check if a given request has access to update a term
351
+ *
352
+ * @param WP_REST_Request $request Full details about the request.
353
+ * @return bool|WP_Error
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;
368
+ }
369
+
370
+ /**
371
+ * Check if a given request has access to delete a term
372
+ *
373
+ * @param WP_REST_Request $request Full details about the request.
374
+ * @return bool|WP_Error
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;
394
+ }
395
+
396
+ /**
397
+ * Get the base path for a term's taxonomy endpoints.
398
+ *
399
+ * @param object|string $taxonomy
400
+ * @return string $base
401
+ */
402
+ public function get_taxonomy_base( $taxonomy ) {
403
+ if ( ! is_object( $taxonomy ) ) {
404
+ $taxonomy = get_taxonomy( $taxonomy );
405
+ }
406
+
407
+ $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
408
+
409
+ return $base;
410
+ }
411
+
412
+ /**
413
+ * Prepare a single term output for response
414
+ *
415
+ * @param obj $item Term object
416
+ * @param WP_REST_Request $request
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 ),
433
+ 'name' => $item->name,
434
+ 'slug' => $item->slug,
435
+ 'taxonomy' => $item->taxonomy,
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
+ /**
454
+ * Prepare links for the request.
455
+ *
456
+ * @param object $term Term object.
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
+ }
478
+ }
479
+
480
+ return $links;
481
+ }
482
+
483
+ /**
484
+ * Get the Term's schema, conforming to JSON Schema
485
+ *
486
+ * @return array
487
+ */
488
+ public function get_item_schema() {
489
+ $schema = array(
490
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
491
+ 'title' => 'term',
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
+ );
544
+ }
545
+ return $this->add_additional_fields_schema( $schema );
546
+ }
547
+
548
+ /**
549
+ * Get the query params for collections
550
+ *
551
+ * @return array
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
+ );
578
+ }
579
+ return $query_params;
580
+ }
581
+
582
+ /**
583
+ * Check that the taxonomy is valid
584
+ *
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
+ }
lib/endpoints/class-wp-rest-users-controller.php ADDED
@@ -0,0 +1,751 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Access users
5
+ */
6
+ class WP_REST_Users_Controller extends WP_REST_Controller {
7
+
8
+ /**
9
+ * Register the routes for the objects of the 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(
34
+ 'methods' => WP_REST_Server::READABLE,
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
+ ),
51
+ array(
52
+ 'methods' => WP_REST_Server::DELETABLE,
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(
62
+ 'methods' => WP_REST_Server::READABLE,
63
+ 'callback' => array( $this, 'get_current_item' ),
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
+ /**
76
+ * Get all users
77
+ *
78
+ * @param WP_REST_Request $request Full details about the request.
79
+ * @return WP_Error|WP_REST_Response
80
+ */
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 );
98
+ if ( is_wp_error( $query ) ) {
99
+ return $query;
100
+ }
101
+
102
+ $users = array();
103
+ foreach ( $query->results as $user ) {
104
+ $data = $this->prepare_item_for_response( $user, $request );
105
+ $users[] = $this->prepare_response_for_collection( $data );
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
+ }
131
+
132
+ return $response;
133
+ }
134
+
135
+ /**
136
+ * Get a single user
137
+ *
138
+ * @param WP_REST_Request $request Full details about the request.
139
+ * @return WP_Error|WP_REST_Response
140
+ */
141
+ public function get_item( $request ) {
142
+ $id = (int) $request['id'];
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 );
150
+ $response = rest_ensure_response( $user );
151
+
152
+ return $response;
153
+ }
154
+
155
+ /**
156
+ * Get the current user
157
+ *
158
+ * @param WP_REST_Request $request Full details about the request.
159
+ * @return WP_Error|WP_REST_Response
160
+ */
161
+ public function get_current_item( $request ) {
162
+ $current_user_id = get_current_user_id();
163
+ if ( empty( $current_user_id ) ) {
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
+ }
174
+
175
+ $response = rest_ensure_response( $response );
176
+ $response->header( 'Location', rest_url( sprintf( '/wp/v2/users/%d', $current_user_id ) ) );
177
+ $response->set_status( 302 );
178
+
179
+ return $response;
180
+ }
181
+
182
+ /**
183
+ * Create a single user
184
+ *
185
+ * @param WP_REST_Request $request Full details about the request.
186
+ * @return WP_Error|WP_REST_Response
187
+ */
188
+ public function create_item( $request ) {
189
+ global $wp_roles;
190
+
191
+ if ( ! empty( $request['id'] ) ) {
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() ) {
202
+ $ret = wpmu_validate_user_signup( $user->user_login, $user->user_email );
203
+ if ( is_wp_error( $ret['errors'] ) && ! empty( $ret['errors']->errors ) ) {
204
+ return $ret['errors'];
205
+ }
206
+ }
207
+
208
+ if ( is_multisite() ) {
209
+ $user_id = wpmu_create_user( $user->user_login, $user->user_pass, $user->user_email );
210
+ if ( ! $user_id ) {
211
+ return new WP_Error( 'rest_user_create', __( 'Error creating new user.' ), array( 'status' => 500 ) );
212
+ }
213
+ $user->ID = $user_id;
214
+ $user_id = wp_update_user( $user );
215
+ if ( is_wp_error( $user_id ) ) {
216
+ return $user_id;
217
+ }
218
+ } else {
219
+ $user_id = wp_insert_user( $user );
220
+ if ( is_wp_error( $user_id ) ) {
221
+ return $user_id;
222
+ }
223
+ $user->ID = $user_id;
224
+ }
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 ) );
237
+
238
+ return $response;
239
+ }
240
+
241
+ /**
242
+ * Update a single user
243
+ *
244
+ * @param WP_REST_Request $request Full details about the request.
245
+ * @return WP_Error|WP_REST_Response
246
+ */
247
+ public function update_item( $request ) {
248
+ $id = (int) $request['id'];
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 ) {
256
+ return new WP_Error( 'rest_user_invalid_email', __( 'Email address is invalid.' ), array( 'status' => 400 ) );
257
+ }
258
+
259
+ if ( ! empty( $request['username'] ) && $request['username'] !== $user->user_login ) {
260
+ return new WP_Error( 'rest_user_invalid_argument', __( "Username isn't editable" ), array( 'status' => 400 ) );
261
+ }
262
+
263
+ if ( ! empty( $request['slug'] ) && $request['slug'] !== $user->user_nicename && get_user_by( 'slug', $request['slug'] ) ) {
264
+ return new WP_Error( 'rest_user_invalid_slug', __( 'Slug is invalid.' ), array( 'status' => 400 ) );
265
+ }
266
+
267
+ if ( ! empty( $request['role'] ) ) {
268
+ $check_permission = $this->check_role_update( $id, $request['role'] );
269
+ if ( is_wp_error( $check_permission ) ) {
270
+ return $check_permission;
271
+ }
272
+ }
273
+
274
+ $user = $this->prepare_item_for_database( $request );
275
+
276
+ // Ensure we're operating on the same user we already checked
277
+ $user->ID = $id;
278
+
279
+ $user_id = wp_update_user( $user );
280
+ if ( is_wp_error( $user_id ) ) {
281
+ return $user_id;
282
+ }
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
+ /**
299
+ * Delete a single user
300
+ *
301
+ * @param WP_REST_Request $request Full details about the request.
302
+ * @return WP_Error|WP_REST_Response
303
+ */
304
+ public function delete_item( $request ) {
305
+ $id = (int) $request['id'];
306
+ $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null;
307
+ $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
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
+ /**
354
+ * Check if a given request has access to read a user
355
+ *
356
+ * @param WP_REST_Request $request Full details about the request.
357
+ * @return bool|WP_Error
358
+ */
359
+ public function get_item_permissions_check( $request ) {
360
+
361
+ $id = (int) $request['id'];
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 ) {
369
+ return true;
370
+ }
371
+
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;
383
+ }
384
+
385
+ /**
386
+ * Check if a given request has access create users
387
+ *
388
+ * @param WP_REST_Request $request Full details about the request.
389
+ * @return bool
390
+ */
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;
398
+ }
399
+
400
+ /**
401
+ * Check if a given request has access update a user
402
+ *
403
+ * @param WP_REST_Request $request Full details about the request.
404
+ * @return bool
405
+ */
406
+ public function update_item_permissions_check( $request ) {
407
+
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;
419
+ }
420
+
421
+ /**
422
+ * Check if a given request has access delete a user
423
+ *
424
+ * @param WP_REST_Request $request Full details about the request.
425
+ * @return bool
426
+ */
427
+ public function delete_item_permissions_check( $request ) {
428
+
429
+ $id = (int) $request['id'];
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;
437
+ }
438
+
439
+ /**
440
+ * Prepare a single user output for response
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';
467
+ $data = $this->filter_response_by_context( $data, $context );
468
+
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
+ /**
480
+ * Prepare links for the request.
481
+ *
482
+ * @param WP_Post $user User object.
483
+ * @return array Links for the given user.
484
+ */
485
+ protected function prepare_links( $user ) {
486
+ $links = array(
487
+ 'self' => array(
488
+ 'href' => rest_url( sprintf( '/wp/v2/users/%d', $user->ID ) ),
489
+ ),
490
+ 'collection' => array(
491
+ 'href' => rest_url( '/wp/v2/users' ),
492
+ ),
493
+ );
494
+
495
+ return $links;
496
+ }
497
+
498
+ /**
499
+ * Prepare a single user for create or update
500
+ *
501
+ * @param WP_REST_Request $request Request object.
502
+ * @return object $prepared_user User object.
503
+ */
504
+ protected function prepare_item_for_database( $request ) {
505
+ $prepared_user = new stdClass;
506
+
507
+ // required arguments.
508
+ if ( isset( $request['email'] ) ) {
509
+ $prepared_user->user_email = $request['email'];
510
+ }
511
+ if ( isset( $request['username'] ) ) {
512
+ $prepared_user->user_login = $request['username'];
513
+ }
514
+ if ( isset( $request['password'] ) ) {
515
+ $prepared_user->user_pass = $request['password'];
516
+ }
517
+
518
+ // optional arguments.
519
+ if ( isset( $request['id'] ) ) {
520
+ $prepared_user->ID = absint( $request['id'] );
521
+ }
522
+ if ( isset( $request['name'] ) ) {
523
+ $prepared_user->display_name = $request['name'];
524
+ }
525
+ if ( isset( $request['first_name'] ) ) {
526
+ $prepared_user->first_name = $request['first_name'];
527
+ }
528
+ if ( isset( $request['last_name'] ) ) {
529
+ $prepared_user->last_name = $request['last_name'];
530
+ }
531
+ if ( isset( $request['nickname'] ) ) {
532
+ $prepared_user->nickname = $request['nickname'];
533
+ }
534
+ if ( isset( $request['slug'] ) ) {
535
+ $prepared_user->user_nicename = $request['slug'];
536
+ }
537
+ if ( isset( $request['description'] ) ) {
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
+
550
+ /**
551
+ * Determine if the current user is allowed to make the desired role change.
552
+ *
553
+ * @param integer $user_id
554
+ * @param string $role
555
+ * @return boolen|WP_Error
556
+ */
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 ) );
573
+ }
574
+
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
+ /**
582
+ * Get the User's schema, conforming to JSON Schema
583
+ *
584
+ * @return array
585
+ */
586
+ public function get_item_schema() {
587
+ $avatar_properties = array();
588
+
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
+ );
718
+ return $this->add_additional_fields_schema( $schema );
719
+ }
720
+
721
+ /**
722
+ * Get the query params for collections
723
+ *
724
+ * @return array
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
+ );
749
+ return $query_params;
750
+ }
751
+ }
lib/infrastructure/class-jsonserializable.php ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,764 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,962 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
license.txt ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 2, June 1991
3
+
4
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
5
+ 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
6
+
7
+ Everyone is permitted to copy and distribute verbatim copies
8
+ of this license document, but changing it is not allowed.
9
+
10
+ Preamble
11
+
12
+ The licenses for most software are designed to take away your
13
+ freedom to share and change it. By contrast, the GNU General Public
14
+ License is intended to guarantee your freedom to share and change free
15
+ software--to make sure the software is free for all its users. This
16
+ General Public License applies to most of the Free Software
17
+ Foundation's software and to any other program whose authors commit to
18
+ using it. (Some other Free Software Foundation software is covered by
19
+ the GNU Library General Public License instead.) You can apply it to
20
+ your programs, too.
21
+
22
+ When we speak of free software, we are referring to freedom, not
23
+ price. Our General Public Licenses are designed to make sure that you
24
+ have the freedom to distribute copies of free software (and charge for
25
+ this service if you wish), that you receive source code or can get it
26
+ if you want it, that you can change the software or use pieces of it
27
+ in new free programs; and that you know you can do these things.
28
+
29
+ To protect your rights, we need to make restrictions that forbid
30
+ anyone to deny you these rights or to ask you to surrender the rights.
31
+ These restrictions translate to certain responsibilities for you if you
32
+ distribute copies of the software, or if you modify it.
33
+
34
+ For example, if you distribute copies of such a program, whether
35
+ gratis or for a fee, you must give the recipients all the rights that
36
+ you have. You must make sure that they, too, receive or can get the
37
+ source code. And you must show them these terms so they know their
38
+ rights.
39
+
40
+ We protect your rights with two steps: (1) copyright the software, and
41
+ (2) offer you this license which gives you legal permission to copy,
42
+ distribute and/or modify the software.
43
+
44
+ Also, for each author's protection and ours, we want to make certain
45
+ that everyone understands that there is no warranty for this free
46
+ software. If the software is modified by someone else and passed on, we
47
+ want its recipients to know that what they have is not the original, so
48
+ that any problems introduced by others will not reflect on the original
49
+ authors' reputations.
50
+
51
+ Finally, any free program is threatened constantly by software
52
+ patents. We wish to avoid the danger that redistributors of a free
53
+ program will individually obtain patent licenses, in effect making the
54
+ program proprietary. To prevent this, we have made it clear that any
55
+ patent must be licensed for everyone's free use or not licensed at all.
56
+
57
+ The precise terms and conditions for copying, distribution and
58
+ modification follow.
59
+
60
+ GNU GENERAL PUBLIC LICENSE
61
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
62
+
63
+ 0. This License applies to any program or other work which contains
64
+ a notice placed by the copyright holder saying it may be distributed
65
+ under the terms of this General Public License. The "Program", below,
66
+ refers to any such program or work, and a "work based on the Program"
67
+ means either the Program or any derivative work under copyright law:
68
+ that is to say, a work containing the Program or a portion of it,
69
+ either verbatim or with modifications and/or translated into another
70
+ language. (Hereinafter, translation is included without limitation in
71
+ the term "modification".) Each licensee is addressed as "you".
72
+
73
+ Activities other than copying, distribution and modification are not
74
+ covered by this License; they are outside its scope. The act of
75
+ running the Program is not restricted, and the output from the Program
76
+ is covered only if its contents constitute a work based on the
77
+ Program (independent of having been made by running the Program).
78
+ Whether that is true depends on what the Program does.
79
+
80
+ 1. You may copy and distribute verbatim copies of the Program's
81
+ source code as you receive it, in any medium, provided that you
82
+ conspicuously and appropriately publish on each copy an appropriate
83
+ copyright notice and disclaimer of warranty; keep intact all the
84
+ notices that refer to this License and to the absence of any warranty;
85
+ and give any other recipients of the Program a copy of this License
86
+ along with the Program.
87
+
88
+ You may charge a fee for the physical act of transferring a copy, and
89
+ you may at your option offer warranty protection in exchange for a fee.
90
+
91
+ 2. You may modify your copy or copies of the Program or any portion
92
+ of it, thus forming a work based on the Program, and copy and
93
+ distribute such modifications or work under the terms of Section 1
94
+ above, provided that you also meet all of these conditions:
95
+
96
+ a) You must cause the modified files to carry prominent notices
97
+ stating that you changed the files and the date of any change.
98
+
99
+ b) You must cause any work that you distribute or publish, that in
100
+ whole or in part contains or is derived from the Program or any
101
+ part thereof, to be licensed as a whole at no charge to all third
102
+ parties under the terms of this License.
103
+
104
+ c) If the modified program normally reads commands interactively
105
+ when run, you must cause it, when started running for such
106
+ interactive use in the most ordinary way, to print or display an
107
+ announcement including an appropriate copyright notice and a
108
+ notice that there is no warranty (or else, saying that you provide
109
+ a warranty) and that users may redistribute the program under
110
+ these conditions, and telling the user how to view a copy of this
111
+ License. (Exception: if the Program itself is interactive but
112
+ does not normally print such an announcement, your work based on
113
+ the Program is not required to print an announcement.)
114
+
115
+ These requirements apply to the modified work as a whole. If
116
+ identifiable sections of that work are not derived from the Program,
117
+ and can be reasonably considered independent and separate works in
118
+ themselves, then this License, and its terms, do not apply to those
119
+ sections when you distribute them as separate works. But when you
120
+ distribute the same sections as part of a whole which is a work based
121
+ on the Program, the distribution of the whole must be on the terms of
122
+ this License, whose permissions for other licensees extend to the
123
+ entire whole, and thus to each and every part regardless of who wrote it.
124
+ Thus, it is not the intent of this section to claim rights or contest
125
+ your rights to work written entirely by you; rather, the intent is to
126
+ exercise the right to control the distribution of derivative or
127
+ collective works based on the Program.
128
+
129
+ In addition, mere aggregation of another work not based on the Program
130
+ with the Program (or with a work based on the Program) on a volume of
131
+ a storage or distribution medium does not bring the other work under
132
+ the scope of this License.
133
+
134
+ 3. You may copy and distribute the Program (or a work based on it,
135
+ under Section 2) in object code or executable form under the terms of
136
+ Sections 1 and 2 above provided that you also do one of the following:
137
+
138
+ a) Accompany it with the complete corresponding machine-readable
139
+ source code, which must be distributed under the terms of Sections
140
+ 1 and 2 above on a medium customarily used for software interchange; or,
141
+
142
+ b) Accompany it with a written offer, valid for at least three
143
+ years, to give any third party, for a charge no more than your
144
+ cost of physically performing source distribution, a complete
145
+ machine-readable copy of the corresponding source code, to be
146
+ distributed under the terms of Sections 1 and 2 above on a medium
147
+ customarily used for software interchange; or,
148
+
149
+ c) Accompany it with the information you received as to the offer
150
+ to distribute corresponding source code. (This alternative is
151
+ allowed only for noncommercial distribution and only if you
152
+ received the program in object code or executable form with such
153
+ an offer, in accord with Subsection b above.)
154
+
155
+ The source code for a work means the preferred form of the work for
156
+ making modifications to it. For an executable work, complete source
157
+ code means all the source code for all modules it contains, plus any
158
+ associated interface definition files, plus the scripts used to
159
+ control compilation and installation of the executable. However, as a
160
+ special exception, the source code distributed need not include
161
+ anything that is normally distributed (in either source or binary
162
+ form) with the major components (compiler, kernel, and so on) of the
163
+ operating system on which the executable runs, unless that component
164
+ itself accompanies the executable.
165
+
166
+ If distribution of executable or object code is made by offering
167
+ access to copy from a designated place, then offering equivalent
168
+ access to copy the source code from the same place counts as
169
+ distribution of the source code, even though third parties are not
170
+ compelled to copy the source along with the object code.
171
+
172
+ 4. You may not copy, modify, sublicense, or distribute the Program
173
+ except as expressly provided under this License. Any attempt
174
+ otherwise to copy, modify, sublicense or distribute the Program is
175
+ void, and will automatically terminate your rights under this License.
176
+ However, parties who have received copies, or rights, from you under
177
+ this License will not have their licenses terminated so long as such
178
+ parties remain in full compliance.
179
+
180
+ 5. You are not required to accept this License, since you have not
181
+ signed it. However, nothing else grants you permission to modify or
182
+ distribute the Program or its derivative works. These actions are
183
+ prohibited by law if you do not accept this License. Therefore, by
184
+ modifying or distributing the Program (or any work based on the
185
+ Program), you indicate your acceptance of this License to do so, and
186
+ all its terms and conditions for copying, distributing or modifying
187
+ the Program or works based on it.
188
+
189
+ 6. Each time you redistribute the Program (or any work based on the
190
+ Program), the recipient automatically receives a license from the
191
+ original licensor to copy, distribute or modify the Program subject to
192
+ these terms and conditions. You may not impose any further
193
+ restrictions on the recipients' exercise of the rights granted herein.
194
+ You are not responsible for enforcing compliance by third parties to
195
+ this License.
196
+
197
+ 7. If, as a consequence of a court judgment or allegation of patent
198
+ infringement or for any other reason (not limited to patent issues),
199
+ conditions are imposed on you (whether by court order, agreement or
200
+ otherwise) that contradict the conditions of this License, they do not
201
+ excuse you from the conditions of this License. If you cannot
202
+ distribute so as to satisfy simultaneously your obligations under this
203
+ License and any other pertinent obligations, then as a consequence you
204
+ may not distribute the Program at all. For example, if a patent
205
+ license would not permit royalty-free redistribution of the Program by
206
+ all those who receive copies directly or indirectly through you, then
207
+ the only way you could satisfy both it and this License would be to
208
+ refrain entirely from distribution of the Program.
209
+
210
+ If any portion of this section is held invalid or unenforceable under
211
+ any particular circumstance, the balance of the section is intended to
212
+ apply and the section as a whole is intended to apply in other
213
+ circumstances.
214
+
215
+ It is not the purpose of this section to induce you to infringe any
216
+ patents or other property right claims or to contest validity of any
217
+ such claims; this section has the sole purpose of protecting the
218
+ integrity of the free software distribution system, which is
219
+ implemented by public license practices. Many people have made
220
+ generous contributions to the wide range of software distributed
221
+ through that system in reliance on consistent application of that
222
+ system; it is up to the author/donor to decide if he or she is willing
223
+ to distribute software through any other system and a licensee cannot
224
+ impose that choice.
225
+
226
+ This section is intended to make thoroughly clear what is believed to
227
+ be a consequence of the rest of this License.
228
+
229
+ 8. If the distribution and/or use of the Program is restricted in
230
+ certain countries either by patents or by copyrighted interfaces, the
231
+ original copyright holder who places the Program under this License
232
+ may add an explicit geographical distribution limitation excluding
233
+ those countries, so that distribution is permitted only in or among
234
+ countries not thus excluded. In such case, this License incorporates
235
+ the limitation as if written in the body of this License.
236
+
237
+ 9. The Free Software Foundation may publish revised and/or new versions
238
+ of the General Public License from time to time. Such new versions will
239
+ be similar in spirit to the present version, but may differ in detail to
240
+ address new problems or concerns.
241
+
242
+ Each version is given a distinguishing version number. If the Program
243
+ specifies a version number of this License which applies to it and "any
244
+ later version", you have the option of following the terms and conditions
245
+ either of that version or of any later version published by the Free
246
+ Software Foundation. If the Program does not specify a version number of
247
+ this License, you may choose any version ever published by the Free Software
248
+ Foundation.
249
+
250
+ 10. If you wish to incorporate parts of the Program into other free
251
+ programs whose distribution conditions are different, write to the author
252
+ to ask for permission. For software which is copyrighted by the Free
253
+ Software Foundation, write to the Free Software Foundation; we sometimes
254
+ make exceptions for this. Our decision will be guided by the two goals
255
+ of preserving the free status of all derivatives of our free software and
256
+ of promoting the sharing and reuse of software generally.
257
+
258
+ NO WARRANTY
259
+
260
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261
+ FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262
+ OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263
+ PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264
+ OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266
+ TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267
+ PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268
+ REPAIR OR CORRECTION.
269
+
270
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272
+ REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273
+ INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274
+ OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275
+ TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276
+ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277
+ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278
+ POSSIBILITY OF SUCH DAMAGES.
279
+
280
+ END OF TERMS AND CONDITIONS
281
+
plugin.php ADDED
@@ -0,0 +1,707 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Plugin Name: WP REST API
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
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' );
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
+
151
+ foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
152
+ $class = ! empty( $post_type->rest_controller_class ) ? $post_type->rest_controller_class : 'WP_REST_Posts_Controller';
153
+
154
+ if ( ! class_exists( $class ) ) {
155
+ continue;
156
+ }
157
+ $controller = new $class( $post_type->name );
158
+ if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
159
+ continue;
160
+ }
161
+
162
+ $controller->register_routes();
163
+
164
+ if ( post_type_supports( $post_type->name, 'custom-fields' ) ) {
165
+ $meta_controller = new WP_REST_Meta_Posts_Controller( $post_type->name );
166
+ $meta_controller->register_routes();
167
+ }
168
+ if ( post_type_supports( $post_type->name, 'revisions' ) ) {
169
+ $revisions_controller = new WP_REST_Revisions_Controller( $post_type->name );
170
+ $revisions_controller->register_routes();
171
+ }
172
+
173
+ foreach ( get_object_taxonomies( $post_type->name, 'objects' ) as $taxonomy ) {
174
+
175
+ if ( empty( $taxonomy->show_in_rest ) ) {
176
+ continue;
177
+ }
178
+
179
+ $posts_terms_controller = new WP_REST_Posts_Terms_Controller( $post_type->name, $taxonomy->name );
180
+ $posts_terms_controller->register_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
+
208
+ if ( ! class_exists( $class ) ) {
209
+ continue;
210
+ }
211
+ $controller = new $class( $taxonomy->name );
212
+ if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
213
+ continue;
214
+ }
215
+
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;
readme.txt ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
7
+ License: GPLv2 or later
8
+ License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
+
10
+ Access your site's data through an easy-to-use HTTP REST API. (Version 2)
11
+
12
+ == Description ==
13
+ WordPress is moving towards becoming a fully-fledged application framework, and we need new APIs. This project was born to create an easy-to-use, easy-to-understand and well-tested framework for creating these APIs, plus creating APIs for core.
14
+
15
+ This plugin provides an easy to use REST API, available via HTTP. Grab your site's data in simple JSON format, including users, posts, taxonomies and more. Retrieving or updating data is as simple as sending a HTTP request.
16
+
17
+ Want to get your site's posts? Simply send a `GET` request to `/wp-json/wp/v2/posts`. Update user with ID 4? Send a `PUT` request to `/wp-json/wp/v2/users/4`. Get all posts with the search term "awesome"? `GET /wp-json/wp/v2/posts?filter[s]=awesome`. It's that easy.
18
+
19
+ WP API exposes a simple yet easy interface to WP Query, the posts API, post meta API, users API, revisions API and many more. Chances are, if you can do it with WordPress, WP API will let you do it.
20
+
21
+ WP API also includes an easy-to-use Javascript API based on Backbone models, allowing plugin and theme developers to get up and running without needing to know anything about the details of getting connected.
22
+
23
+ Check out [our documentation][docs] for information on what's available in the API and how to use it. We've also got documentation on extending the API with extra data for plugin and theme developers!
24
+
25
+ All tickets for the project are being tracked on [GitHub][]. You can also take a look at the [recent updates][] for the project.
26
+
27
+ [docs]: http://v2.wp-api.org/
28
+ [GitHub]: https://github.com/WP-API/WP-API
29
+ [recent updates]: http://make.wp-api.org/
30
+
31
+ == Installation ==
32
+
33
+ Drop this directory in and activate it.
34
+
35
+ For full-flavoured API support, you'll need to be using pretty permalinks to use the plugin, as it uses custom rewrite rules to power the API.
36
+
37
+ == Changelog ==
38
+
39
+ = Version 2.0 Beta 1 =
40
+
41
+ Partial rewrite and evolution of the REST API to prepare for core integration.
42
+
43
+ For versions 0.x through 1.x, see the [legacy plugin changelog](https://wordpress.org/plugins/json-rest-api/changelog/).
wp-api.js ADDED
@@ -0,0 +1,987 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function( window, undefined ) {
2
+
3
+ 'use strict';
4
+
5
+ function WP_API() {
6
+ this.models = {};
7
+ this.collections = {};
8
+ this.views = {};
9
+ }
10
+
11
+ window.wp = window.wp || {};
12
+ wp.api = wp.api || new WP_API();
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
+
31
+ Date.prototype.toISOString = function() {
32
+ return this.getUTCFullYear() +
33
+ '-' + pad( this.getUTCMonth() + 1 ) +
34
+ '-' + pad( this.getUTCDate() ) +
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
+ }
123
+ });
124
+
125
+ return attributes;
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;
139
+ }
140
+
141
+ var timestamp = wp.api.utils.parseISO8601( response[key] );
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
+
150
+ return response;
151
+ }
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
+ */
165
+ parent: function() {
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?
180
+ */
181
+ parentModel = new this.parentModel();
182
+ }
183
+
184
+ // Can we get this from its collection?
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 ) {
221
+ xhr.setRequestHeader( 'X-WP-Nonce', WP_API_Settings.nonce );
222
+
223
+ if ( beforeSend ) {
224
+ return beforeSend.apply( this, arguments );
225
+ }
226
+ };
227
+ }
228
+
229
+ return Backbone.sync( method, model, options );
230
+ }
231
+ }
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,
619
+ name: '',
620
+ 'public': true,
621
+ 'protected': false,
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 = {
666
+ data: {},
667
+ currentPage: null,
668
+ totalPages: null,
669
+ totalObjects: null
670
+ };
671
+ },
672
+
673
+ /**
674
+ * Overwrite Backbone.Collection.sync to pagination state based on response headers.
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 ) {
728
+ return success.apply( this, arguments );
729
+ }
730
+ };
731
+ }
732
+
733
+ return Backbone.sync( method, model, options );
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 || {};
744
+ options.data = options.data || {};
745
+
746
+ _.extend( options.data, this.state.data );
747
+
748
+ if ( typeof options.data.page === 'undefined' ) {
749
+ if ( ! this.hasMore() ) {
750
+ return false;
751
+ }
752
+
753
+ if ( this.state.currentPage === null || this.state.currentPage <= 1 ) {
754
+ options.data.page = 2;
755
+ } else {
756
+ options.data.page = this.state.currentPage + 1;
757
+ }
758
+ }
759
+
760
+ return this.fetch( options );
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 ||
770
+ this.state.totalObjects === null ||
771
+ this.state.currentPage === null ) {
772
+ return null;
773
+ } else {
774
+ return ( this.state.currentPage < this.state.totalPages );
775
+ }
776
+ }
777
+ }
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 */
958
+ {
959
+ model: wp.api.models.Revision,
960
+
961
+ parent: null,
962
+
963
+ /**
964
+ * @class Represent an array of revisions
965
+ * @augments Backbone.Collection
966
+ * @constructs
967
+ */
968
+ initialize: function( models, options ) {
969
+ BaseCollection.prototype.initialize.apply( this, arguments );
970
+
971
+ if ( options && options.parent ) {
972
+ this.parent = options.parent;
973
+ }
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 );