From c3098327b2406a8b30d7dca1cde0fc6df3cf5153 Mon Sep 17 00:00:00 2001 From: Eric Lanehart Date: Fri, 7 Mar 2014 18:06:12 -0500 Subject: [PATCH 001/515] Add descriptions to taxonomy term data --- lib/class-wp-json-taxonomies.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index a4424113dd..51412e6943 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -186,13 +186,14 @@ public function add_term_data( $data, $post, $context ) { protected function prepare_term( $term, $type, $context = 'view' ) { $base_url = '/posts/types/' . $type . '/taxonomies/' . $term->taxonomy . '/terms'; $data = array( - 'ID' => (int) $term->term_taxonomy_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'parent' => (int) $term->parent, - 'count' => (int) $term->count, - 'link' => get_term_link( $term, $term->taxonomy ), - 'meta' => array( + 'ID' => (int) $term->term_taxonomy_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'description' => $term->description, + 'parent' => (int) $term->parent, + 'count' => (int) $term->count, + 'link' => get_term_link( $term, $term->taxonomy ), + 'meta' => array( 'links' => array( 'collection' => json_url( $base_url ), 'self' => json_url( $base_url . '/' . $term->term_id ), From f3885659b43d8bbe993791652206c3dd4480ca1c Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 6 Apr 2014 14:17:33 +1000 Subject: [PATCH 002/515] Note that Chassis requires local-config.php --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d9e2c97579..e585f07561 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ testing environment in a few easy steps: ```bash cd api-tester + cp local-config-sample.php local-config.php mkdir -p content/plugins git clone git@github.com:WP-API/WP-API.git content/plugins/json-rest-api ``` From 8cc8b8a16f03a2faca33050d0fce420014c1bcc9 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 6 Apr 2014 14:27:27 +1000 Subject: [PATCH 003/515] Clarify that content/themes/ needs to be created Also removes local-config.php instructions, as newer Chassis versions don't require it. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e585f07561..e6d39bff1b 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ testing environment in a few easy steps: ```bash cd api-tester - cp local-config-sample.php local-config.php - mkdir -p content/plugins + mkdir -p content/plugins content/themes + cp -r wp/wp-content/themes/* content/themes git clone git@github.com:WP-API/WP-API.git content/plugins/json-rest-api ``` From 2ddef509d2c06d5c8a38bc31af7dafb26fdb9337 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 6 Apr 2014 14:32:43 +1000 Subject: [PATCH 004/515] Fix link to issue tracker We've been using GitHub for a while now. --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e6d39bff1b..9a2fdc2c5c 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,9 @@ For testing, you'll need a little bit more: ## Issue Tracking -All tickets for the project are being tracked on the [GSoC Trac][]. Make sure -you use the JSON REST API component. +All tickets for the project are being tracked on [GitHub][]. Previous issues can +be found on the [GSOC Trac][] issue tracker, however new issues should not be +filed there. -[GSoC Trac]: https://gsoc.trac.wordpress.org/query?component=JSON+REST+API +[GitHub]: https://github.com/WP-API/WP-API +[GSOC Trac]: https://gsoc.trac.wordpress.org/query?component=JSON+REST+API From c909611ee480b5af85501ab87f962daf29bc5a22 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 6 Apr 2014 14:45:41 +1000 Subject: [PATCH 005/515] Move filter registration out of CPT constructor --- lib/class-wp-json-customposttype.php | 12 ++++++++++-- plugin.php | 3 +-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-json-customposttype.php b/lib/class-wp-json-customposttype.php index 8d7dabb55f..b3c942cd51 100644 --- a/lib/class-wp-json-customposttype.php +++ b/lib/class-wp-json-customposttype.php @@ -38,10 +38,18 @@ public function __construct(WP_JSON_ResponseHandler $server) { return; } + parent::__construct($server); + } + + /** + * Add actions and filters for the post type + * + * This method should be called after instantiation to automatically add the + * required filters for the post type. + */ + public function register_filters() { add_filter( 'json_endpoints', array( $this, 'register_routes' ) ); add_filter( 'json_post_type_data', array( $this, 'type_archive_link' ), 10, 2 ); - - parent::__construct($server); } /** diff --git a/plugin.php b/plugin.php index 1f5cc573c5..6820821ab6 100644 --- a/plugin.php +++ b/plugin.php @@ -51,8 +51,7 @@ function json_api_default_filters($server) { // Pages $wp_json_pages = new WP_JSON_Pages($server); - add_filter( 'json_endpoints', array( $wp_json_pages, 'register_routes' ), 1 ); - add_filter( 'json_post_type_data', array( $wp_json_pages, 'type_archive_link' ), 10, 2 ); + $wp_json_pages->register_filters(); // Media $wp_json_media = new WP_JSON_Media($server); From a9d5c4762970ef7ad275184fd9ae483924ac3a57 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 6 Apr 2014 14:47:13 +1000 Subject: [PATCH 006/515] Add call to register_filters in documentation Missed in #126. --- docs/guides/extending.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guides/extending.md b/docs/guides/extending.md index 8f6124a5af..fad216325e 100644 --- a/docs/guides/extending.md +++ b/docs/guides/extending.md @@ -268,6 +268,7 @@ hooking and more for you: require_once dirname( __FILE__ ) . '/class-myplugin-api-mytype.php'; $myplugin_api_mytype = new MyPlugin_API_MyType( $server ); + $myplugin->register_filters(); } add_action( 'wp_json_server_before_serve', 'myplugin_api_init' ); From 1c0a4f4209c53e1c82a276bc5736fc3d95675165 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 6 Apr 2014 16:03:28 +1000 Subject: [PATCH 007/515] Update to 0.9 --- CHANGELOG.md | 143 +++++++++++++++++++++++++++++++++++ lib/class-wp-json-server.php | 2 +- plugin.php | 2 +- 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46a99d1df3..6c59547b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,148 @@ # Changelog +## 0.9 + +- Move from `wp-json.php/` to `wp-json/` + + **This breaks backwards compatibility** and requires any clients to now use + `wp-json/`, or preferably the new RSD/Link headers. + + (props @rmccue, @matrixik, [#46][gh-46], [#96][gh-96], [#106][gh-106]) + +- Move filter registration out of CPT constructor. CPT subclasses now require + you to call `$myobject->register_filters()`, in order to move global state out + of the constructor. + + **This breaks backwards compatibility** and requires any subclassing to now + call `$myobject->register_filters()` + + (props @rmccue, @thenbrent, [#42][gh-42], [#126][gh-126]) + +- Introduce Response/ResponseInterface + + Endpoints that need to set headers or response codes should now return a + `WP_JSON_Response` rather than using the server methods. + `WP_JSON_ResponseInterface` may also be used for more flexible use of the + response methods. + + **Deprecation warning:** Calling `WP_JSON_Server::header`, + `WP_JSON_Server::link_header` and `WP_JSON_Server::query_navigation_headers` + is now deprecated. This will be removed in 1.0. + + (props @rmccue, [#33][gh-33]) + +- Change all semiCamelCase names to underscore_case. + + **Deprecation warning**: Any calls to semiCamelCase methods require any + subclassing to update method references. This will be removed in 1.0. + + (props @osiux, [#36][gh-36], [#82][gh-82]) + +- Add multisite compatibility. If the plugin is network activated, the plugin is + now activated once-per-site, so `wp-json/` is always site-local. + + (props @rachelbaker, [#48][gh-48], [#49][gh-49]) + +- Add RSD and Link headers for discovery + + (props @rmccue, [#40][gh-40]) + +- WP_JSON_Posts->prepare_author() now verifies the `$user` object is set. + + (props @rachelbaker, [#51][gh-51], [#54][gh-54]) + +- Added unit testing framework. Currently only a smaller number of tests, but we + plan to increase this significantly as soon as possible. + + (props @tierra, @osiux, [#65][gh-65], [#76][gh-76], [#84][gh-84]) + +- Link collection filtering docs to URL formatting guide. + + (props @kadamwhite, [#74][gh-74]) + +- Remove hardcoded `/pages` references from `WP_JSON_Pages` + + (props @rmccue, @thenbrent, [#28][gh-28], [#78][gh-78]) + +- Fix compatibility with `DateTime::createFromFormat` on PHP 5.2 + + (props @osiux, [#52][gh-52], [#79][gh-79]) + +- Document that `WP_JSON_CustomPostType::__construct()` requires a param of type + `WP_JSON_ResponseHandler`. + + (props @tlovett1, [#88][gh-88]) + +- Add timezone parameter to WP_JSON_DateTime::createFromFormat() + + (props @royboy789, @rachelbaker, [#85][gh-85], [#87][gh-87]) + +- Remove IXR references. `IXR_Error` is no longer accepted as a return value. + + **This breaks backwards compatibility** and requires anyone returning + `IXR_Error` objects to now return `WP_Error` or `WP_JSON_ResponseInterface` + objects. + + (props @rmccue, [#50][gh-50], [#77][gh-77]) + +- Fix bugs with attaching featured images to posts: + - `WP_JSON_Media::attachThumbnail()` should do nothing if `$update` is false + without a post ID + - The post ID must be fetched from the `$post` array. + + (props @Webbgaraget, [#55][gh-55]) + +- Don't declare `jsonSerialize` on ResponseInterface + + (props @rmccue, [#97][gh-97]) + +- Allow JSON post creation/update for `WP_JSON_CustomPostType` + + (props @tlovett1, [#90][gh-90], [#108][gh-108]) + +- Return null if post doesn't have an excerpt + + (props @rachelbacker, [#72][gh-72]) + +- Fix link to issue tracker in README + + (props @rmccue, @tobych, [#125][gh-125]) + +[View all changes](https://github.com/rmccue/WP-API/compare/0.8...0.9) + +[gh-28]: https://github.com/WP-API/WP-API/issues/28 +[gh-33]: https://github.com/WP-API/WP-API/issues/33 +[gh-36]: https://github.com/WP-API/WP-API/issues/36 +[gh-40]: https://github.com/WP-API/WP-API/issues/40 +[gh-42]: https://github.com/WP-API/WP-API/issues/42 +[gh-46]: https://github.com/WP-API/WP-API/issues/46 +[gh-48]: https://github.com/WP-API/WP-API/issues/48 +[gh-49]: https://github.com/WP-API/WP-API/issues/49 +[gh-50]: https://github.com/WP-API/WP-API/issues/50 +[gh-51]: https://github.com/WP-API/WP-API/issues/51 +[gh-52]: https://github.com/WP-API/WP-API/issues/52 +[gh-54]: https://github.com/WP-API/WP-API/issues/54 +[gh-55]: https://github.com/WP-API/WP-API/issues/55 +[gh-65]: https://github.com/WP-API/WP-API/issues/65 +[gh-72]: https://github.com/WP-API/WP-API/issues/72 +[gh-74]: https://github.com/WP-API/WP-API/issues/74 +[gh-76]: https://github.com/WP-API/WP-API/issues/76 +[gh-77]: https://github.com/WP-API/WP-API/issues/77 +[gh-78]: https://github.com/WP-API/WP-API/issues/78 +[gh-79]: https://github.com/WP-API/WP-API/issues/79 +[gh-82]: https://github.com/WP-API/WP-API/issues/82 +[gh-84]: https://github.com/WP-API/WP-API/issues/84 +[gh-85]: https://github.com/WP-API/WP-API/issues/85 +[gh-87]: https://github.com/WP-API/WP-API/issues/87 +[gh-88]: https://github.com/WP-API/WP-API/issues/88 +[gh-90]: https://github.com/WP-API/WP-API/issues/90 +[gh-96]: https://github.com/WP-API/WP-API/issues/96 +[gh-97]: https://github.com/WP-API/WP-API/issues/97 +[gh-106]: https://github.com/WP-API/WP-API/issues/106 +[gh-108]: https://github.com/WP-API/WP-API/issues/108 +[gh-125]: https://github.com/WP-API/WP-API/issues/125 +[gh-126]: https://github.com/WP-API/WP-API/issues/126 + ## 0.8 - Add compatibility layer for JsonSerializable. You can now return arbitrary objects from endpoints and use the `jsonSerialize()` method to return the data diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index cb4a53daf9..e38428446f 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -5,7 +5,7 @@ * Contains the WP_JSON_Server class. * * @package WordPress - * @version 0.8 + * @version 0.9 */ require_once ABSPATH . 'wp-admin/includes/admin.php'; diff --git a/plugin.php b/plugin.php index 6820821ab6..783505e31a 100644 --- a/plugin.php +++ b/plugin.php @@ -4,7 +4,7 @@ * Description: JSON-based REST API for WordPress, developed as part of GSoC 2013. * Author: Ryan McCue * Author URI: http://ryanmccue.info/ - * Version: 0.8 + * Version: 0.9 * Plugin URI: https://github.com/rmccue/WP-API */ include_once( dirname( __FILE__ ) . '/lib/class-jsonserializable.php' ); From f12d0c04ac83ba49268392f7721e2dc34be05f66 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 6 Apr 2014 21:54:18 +1000 Subject: [PATCH 008/515] Move the test helper to a subdirectory This stops it from appearing in the plugins list. --- testhelper.php => tests/helper.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename testhelper.php => tests/helper.php (100%) diff --git a/testhelper.php b/tests/helper.php similarity index 100% rename from testhelper.php rename to tests/helper.php From 2bbb1af1cfbe5f1a69a5d8f70c413620478594ee Mon Sep 17 00:00:00 2001 From: Toby Champion Date: Fri, 28 Mar 2014 21:17:38 -0700 Subject: [PATCH 009/515] Add User endpoint (no user_meta handling) --- lib/class-wp-json-server.php | 8 -- lib/class-wp-json-users.php | 269 +++++++++++++++++++++++++++++++++++ plugin.php | 5 + 3 files changed, 274 insertions(+), 8 deletions(-) create mode 100644 lib/class-wp-json-users.php diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index e38428446f..e605e2b95a 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -295,16 +295,8 @@ public function get_routes() { '/' => array( array( $this, 'get_index' ), self::READABLE ), // Users - '/users' => array( - array( '__return_null', self::READABLE ), - array( '__return_null', self::CREATABLE | self::ACCEPT_JSON ), - ), // /users/me is an alias, and simply redirects to /users/ '/users/me' => array( '__return_null', self::ALLMETHODS ), - '/users/(?P\d+)' => array( - array( '__return_null', self::READABLE ), - array( '__return_null', self::CREATABLE | self::ACCEPT_JSON ), - ), ); $endpoints = apply_filters( 'json_endpoints', $endpoints ); diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php new file mode 100644 index 0000000000..3db13948fc --- /dev/null +++ b/lib/class-wp-json-users.php @@ -0,0 +1,269 @@ +server = $server; + } + + /** + * Register the user-related routes + * + * @param array $routes Existing routes + * @return array Modified routes + */ + public function registerRoutes( $routes ) { + $user_routes = array( + // User endpoints + '/users' => array( + array( array( $this, 'get_users' ), WP_JSON_Server::READABLE ), + ), + '/users/(?P\d+)' => array( + array( array( $this, 'get_user' ), WP_JSON_Server::READABLE ), + array( array( $this, 'edit_user' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), + array( array( $this, 'delete_user' ), WP_JSON_Server::DELETABLE ), + ) + ); + return array_merge( $routes, $user_routes ); + } + + /** + * Retrieve users. + * + * The optional $filter parameter... TODO: Not implemented! + * Accepted keys are... TODO: Not implemented! + * The optional $fields parameter... TODO: Not implemented! + * + * @param array $filter optional + * @param string $context optional + * @param string $type optional + * @param int $page optional + * @return array contains a collection of User entities. + */ + public function get_users( $filter = array(), $context = 'view', $type = 'user', $page = 1 ) { + + if ( ! current_user_can( 'edit_users' ) ) { + return new WP_Error( 'json_cannot_get', __( 'Sorry, you are not allowed to get users.' ), array( 'status' => 401 ) ); + } + + $args = array( 'orderby' => 'user_login', 'order' => 'ASC' ); + $user_query = new WP_User_Query( $args ); + $struct = array(); + if ( ! empty( $user_query->results ) ) { + foreach ( $user_query->results as $user ) { + $struct[] = $this->prepare_user( $user, $context ); + } + } else { + return array(); + } + return $struct; + } + + /** + * Retrieve a user. + * + * @param int $id User ID + * @param string $context + * @return response + */ + public function get_user( $id, $context = 'view' ) { + $id = (int) $id; + + if ( ! current_user_can( 'edit_users' ) ) { + return new WP_Error( 'json_cannot_get', __( 'Sorry, you are not allowed to get users.' ), array( 'status' => 401 ) ); + } + + if ( empty( $id ) ) + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); + + // http://codex.wordpress.org/Function_Reference/get_userdata + $user = get_userdata( $id ); + + if ( empty( $user->ID ) ) + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); + + // Link headers (see RFC 5988) + + $response = new WP_JSON_Response(); + // user model doesn't appear to include a last-modified date + // $response->header( 'Last-Modified', mysql2date( 'D, d M Y H:i:s', $user->TBW ) . 'GMT' ); + + $user = $this->prepare_user( $user, $context ); + if ( is_wp_error( $user ) ) + return $user; + + $response->set_data( $user ); + return $response; + } + + /** + * + * Prepare a User entity from a WP_User instance. + * + * @param WP_User $user + * @param string $context + * @return array + */ + protected function prepare_user( $user, $context = 'view' ) { + // We're ignoring $fields for now, so you get all these fields + // http://codex.wordpress.org/Function_Reference/get_metadata + // http://code.tutsplus.com/articles/mastering-wordpress-meta-data-understanding-and-using-arrays--wp-34596 + $user_fields = array( + + // As per https://github.com/WP-API/WP-API/blob/master/docs/schema.md#user + 'ID' => $user->ID, + 'name' => $user->display_name, + 'slug' => $user->user_nicename, + 'URL' => $user->user_url, // TODO: this is called 'Website' in the Wordpress users page. Use that? + // Read-only/derived + 'avatar' => $this->server->get_avatar_url( $user->user_email ), + + // Extra stuff that seems important and isn't in user_meta + 'username' => $user->user_login, + 'email' => $user->user_email, + 'registered' => $user->user_registered, + + // TODO: There's also user_activation_key, user_status, password... roles + 'meta' => array( + 'links' => array( + 'self' => json_url( '/users/' . $user->ID ), + 'archives' => json_url( '/users/' . $user->ID . '/posts' ), + ), + ), + ); + return $user_fields; + } + + /** + * Edit a user. + * + * The $data parameter only needs to contain fields that should be changed. + * All other fields will retain their existing values. + * + * @param int $id User ID to edit + * @param array $data Data construct + * @param array $_headers Header data + * @return true on success + */ + function edit_user( $id, $data, $_headers = array() ) { + $id = (int) $id; + + if ( empty( $id ) ) + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID (EMPTY).' ), array( 'status' => 404 ) ); + + // http://codex.wordpress.org/Function_Reference/get_userdata + $user = get_userdata( $id ); // returns False on failure + + if ( ! $user ) + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID (COULD NOT LOAD).' ), array( 'status' => 404 ) ); + + // Permissions check + if ( ! current_user_can( 'edit_users' ) ) { + return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => 401 ) ); + } + + // Update attributes of the user from $data + $retval = $this->update_user( $user, $data ); + if ( is_wp_error( $retval ) ) { + return $retval; + } + + // TBD Pre-insert/update hook (I don't understand what one of those is yet) + + // Update the user in the database + // http://codex.wordpress.org/Function_Reference/wp_update_user + $retval = wp_update_user( $user ); + if ( is_wp_error( $retval ) ) { + return $retval; + } + + // http://codex.wordpress.org/Function_Reference/do_action + do_action( 'json_insert_user', $user, $data, true ); // $update is always true + + return $this->get_user( $id ); + } + + /** + * Delete a user. + * + * @param int $id + * @param bool force + * @return true on success + */ + public function delete_user( $id, $force = false ) { + $id = (int) $id; + + if ( empty( $id ) ) + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); + + // Permissions check + if ( ! current_user_can( 'edit_users' ) ) { + return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => 401 ) ); + } + + $user = get_userdata( $id ); + + if ( ! $user ) + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); + + // https://codex.wordpress.org/Function_Reference/wp_delete_user + // TODO: Allow posts to be reassigned (see the docs for wp_delete_user) - use a HTTP parameter? + $result = wp_delete_user( $id ); + + if ( ! $result ) + return new WP_Error( 'json_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 500 ) ); + + } + + /** + * + * Update a WP_User instance from a User entity. + * + * @param WP_User $user + * @param $data + */ + protected function update_user( $user, $data ) { + + // Won't let them update these fields: ID, login, pass, registered (silently ignored) + // TODO: Raise an exception if they try to update those? Always ignore ID though. + + // Note that you can pass wp_update_user() an array of fields to + // update; we won't bother using it as they don't match the User entity + // and it's just one more level of indirection to maintain. + + // https://github.com/WP-API/WP-API/blob/master/docs/schema.md#user + // http://codex.wordpress.org/Class_Reference/WP_User + // http://wpsmith.net/2012/wp/an-introduction-to-wp_user-class/ + + // There are more fields in WP_User we might + + if ( ! empty( $data['name'] ) ) { + $user->display_name = $data[ 'name' ]; + } + if ( ! empty( $data['slug'] ) ) { + $user->user_nicename = $data[ 'slug' ]; + } + if ( ! empty( $data['URL'] ) ) { + $user->user_url = $data[ 'URL' ]; + } + // ignore avatar - read-only + // ignore username - can't change this + if ( ! empty( $data['email'] ) ) { + $user->user_email = $data['email']; + } + // ignore registered - probably no need + + } + +} diff --git a/plugin.php b/plugin.php index 783505e31a..03925453d3 100644 --- a/plugin.php +++ b/plugin.php @@ -16,6 +16,7 @@ include_once( dirname( __FILE__ ) . '/lib/class-wp-json-response.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-posts.php' ); +include_once( dirname( __FILE__ ) . '/lib/class-wp-json-users.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-customposttype.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-pages.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-media.php' ); @@ -49,6 +50,10 @@ function json_api_default_filters($server) { $wp_json_posts = new WP_JSON_Posts($server); add_filter( 'json_endpoints', array( $wp_json_posts, 'register_routes' ), 0 ); + // Users + $wp_json_users = new WP_JSON_Users($server); + add_filter( 'json_endpoints', array( $wp_json_users, 'registerRoutes' ), 0 ); + // Pages $wp_json_pages = new WP_JSON_Pages($server); $wp_json_pages->register_filters(); From 39d1b32dd1c9ca711881d903826ced60d66f6514 Mon Sep 17 00:00:00 2001 From: Toby Champion Date: Sat, 12 Apr 2014 07:21:11 -0700 Subject: [PATCH 010/515] Handle POST to create a user --- lib/class-wp-json-users.php | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 3db13948fc..6912e01dfd 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -28,6 +28,7 @@ public function registerRoutes( $routes ) { // User endpoints '/users' => array( array( array( $this, 'get_users' ), WP_JSON_Server::READABLE ), + array( array( $this, 'new_user' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), ), '/users/(?P\d+)' => array( array( array( $this, 'get_user' ), WP_JSON_Server::READABLE ), @@ -194,6 +195,72 @@ function edit_user( $id, $data, $_headers = array() ) { return $this->get_user( $id ); } + /** + * Create a new user. + * + * @param $data + * @return mixed + */ + public function new_user( $data ) { + # TODO: Use WP_User here, and refactor so we're sharing code with edit_user + # You can provide a WP_User to wp_insert_user. But the semantics of using WP_User to create a new + # user aren't defined well in the documentation: it doesn't even tell you how to create a WP_User without + # using an ID. So we won't try to do that. + # + # https://codex.wordpress.org/Function_Reference/wp_insert_user - provide complete user_data + # https://codex.wordpress.org/Function_Reference/wp_create_user - just takes username, password, email + # https://codex.wordpress.org/Class_Reference/WP_User + # http://tommcfarlin.com/create-a-user-in-wordpress/ + + if ( empty( $data['username'] )) { + return new WP_Error( 'json_user_username_missing', __( 'No username supplied.'), array( 'status' => 400 ) ); + } + if ( empty( $data['password'] )) { + return new WP_Error( 'json_user_password_missing', __( 'No password supplied.' ), array( 'status' => 400 ) ); + } + if ( empty( $data['email'] )) { + return new WP_Error( 'json_user_email_missing', __( 'No email supplied.' ), array( 'status' => 400 ) ); + } + + $userdata = array( + // These are the required fields for wp_insert_user + 'user_login' => $data['username'], // must be unique + 'user_pass' => $data['password'], // we're sending this in plain text + 'user_email' => $data['email'], // must be unique + ); + + if ( ! empty( $data['slug'] ) ) { + $userdata['user_nicename'] = $data['slug']; // TODO: This is made unique eg to Fred-3 - what to do? + } + if ( ! empty( $data['name'] ) ) { + $userdata['display_name'] = $data['name']; + } + if ( ! empty( $data['URL'] ) ) { + $userdata['user_url'] = $data['URL']; + } + + $result = wp_insert_user( $userdata ); + // TODO: Send appropriate HTTP error codes along with the JSON rendering of the WP_Error we send back + // These are the errors wp_insert_user() might return (from the wp_create_user documentation) + // - empty_user_login, Cannot create a user with an empty login name. => 400 + // - existing_user_login, This username is already registered. => 409 Conflict + // - existing_user_email, This email address is already registered. => 409 Conflict + // http://stackoverflow.com/questions/942951/rest-api-error-return-good-practices + // http://stackoverflow.com/questions/3825990/http-response-code-for-post-when-resource-already-exists + // http://soabits.blogspot.com/2013/05/error-handling-considerations-and-best.html + // TODO: Some other error codes above should be changed to 400 from 404. And in other resources. Just sayin'. + if ( $result instanceof WP_Error ) { + return $result; + } else { + $user_id = $result; + } + + $response = $this->get_user( $user_id ); + $response->set_status( 201 ); + $response->header( 'Location', json_url( '/users/' . $user_id ) ); + return $response; + } + /** * Delete a user. * From 7c43da781250ebfe9516459ea813e0a757081af4 Mon Sep 17 00:00:00 2001 From: Toby Champion Date: Sat, 12 Apr 2014 17:28:48 -0700 Subject: [PATCH 011/515] Add calls to apply_filters for update and create --- lib/class-wp-json-users.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 6912e01dfd..d143aaa2aa 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -180,7 +180,8 @@ function edit_user( $id, $data, $_headers = array() ) { return $retval; } - // TBD Pre-insert/update hook (I don't understand what one of those is yet) + // Pre-update hook + $user = apply_filters( 'pre_wp_json_update_user', $user, $id, $data, $_headers ); // Update the user in the database // http://codex.wordpress.org/Function_Reference/wp_update_user @@ -239,6 +240,14 @@ public function new_user( $data ) { $userdata['user_url'] = $data['URL']; } + + // Pre-insert hook + // TODO: Or json_pre_insert_user? Or insert rather than create? "Insert" seems to mean create or edit in WP...? + $can_insert = apply_filters( 'pre_wp_json_create_user', $userdata, $data ); + if ( is_wp_error( $can_insert ) ) { + return $can_insert; + } + $result = wp_insert_user( $userdata ); // TODO: Send appropriate HTTP error codes along with the JSON rendering of the WP_Error we send back // These are the errors wp_insert_user() might return (from the wp_create_user documentation) @@ -290,7 +299,6 @@ public function delete_user( $id, $force = false ) { if ( ! $result ) return new WP_Error( 'json_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 500 ) ); - } /** From b1044c4370126928f1aa732d31dcc70d1b87a33b Mon Sep 17 00:00:00 2001 From: Toby Champion Date: Sat, 12 Apr 2014 18:43:36 -0700 Subject: [PATCH 012/515] Fix HTTP status code usage; use constants for them Also updates some error messages --- lib/class-wp-json-users.php | 60 ++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index d143aaa2aa..953df70a6e 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -1,5 +1,15 @@ 401 ) ); + return new WP_Error( 'json_cannot_get', __( 'Sorry, you are not allowed to get users.' ), + array( 'status' => HTTP_STATUS_FORBIDDEN ) ); } $args = array( 'orderby' => 'user_login', 'order' => 'ASC' ); @@ -82,17 +93,20 @@ public function get_user( $id, $context = 'view' ) { $id = (int) $id; if ( ! current_user_can( 'edit_users' ) ) { - return new WP_Error( 'json_cannot_get', __( 'Sorry, you are not allowed to get users.' ), array( 'status' => 401 ) ); + return new WP_Error( 'json_cannot_get', __( 'Sorry, you are not allowed to get users.' ), + array( 'status' => HTTP_STATUS_FORBIDDEN ) ); } if ( empty( $id ) ) - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), + array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); // http://codex.wordpress.org/Function_Reference/get_userdata $user = get_userdata( $id ); if ( empty( $user->ID ) ) - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), + array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); // Link headers (see RFC 5988) @@ -161,17 +175,20 @@ function edit_user( $id, $data, $_headers = array() ) { $id = (int) $id; if ( empty( $id ) ) - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID (EMPTY).' ), array( 'status' => 404 ) ); + return new WP_Error( 'json_user_invalid_id', __( 'User ID must be supplied.' ), + array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); // http://codex.wordpress.org/Function_Reference/get_userdata $user = get_userdata( $id ); // returns False on failure if ( ! $user ) - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID (COULD NOT LOAD).' ), array( 'status' => 404 ) ); + return new WP_Error( 'json_user_invalid_id', __( 'User ID must be an integer.' ), + array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); // Permissions check if ( ! current_user_can( 'edit_users' ) ) { - return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => 401 ) ); + return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), + array( 'status' => HTTP_STATUS_FORBIDDEN ) ); } // Update attributes of the user from $data @@ -214,13 +231,16 @@ public function new_user( $data ) { # http://tommcfarlin.com/create-a-user-in-wordpress/ if ( empty( $data['username'] )) { - return new WP_Error( 'json_user_username_missing', __( 'No username supplied.'), array( 'status' => 400 ) ); + return new WP_Error( 'json_user_username_missing', __( 'No username supplied.'), + array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); } if ( empty( $data['password'] )) { - return new WP_Error( 'json_user_password_missing', __( 'No password supplied.' ), array( 'status' => 400 ) ); + return new WP_Error( 'json_user_password_missing', __( 'No password supplied.' ), + array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); } if ( empty( $data['email'] )) { - return new WP_Error( 'json_user_email_missing', __( 'No email supplied.' ), array( 'status' => 400 ) ); + return new WP_Error( 'json_user_email_missing', __( 'No email supplied.' ), + array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); } $userdata = array( @@ -250,14 +270,14 @@ public function new_user( $data ) { $result = wp_insert_user( $userdata ); // TODO: Send appropriate HTTP error codes along with the JSON rendering of the WP_Error we send back + // TODO: I guess we can just add/overwrite the 'status' code in there ourselves... nested WP_Error? // These are the errors wp_insert_user() might return (from the wp_create_user documentation) - // - empty_user_login, Cannot create a user with an empty login name. => 400 - // - existing_user_login, This username is already registered. => 409 Conflict - // - existing_user_email, This email address is already registered. => 409 Conflict + // - empty_user_login, Cannot create a user with an empty login name. => BAD_REQUEST + // - existing_user_login, This username is already registered. => CONFLICT + // - existing_user_email, This email address is already registered. => CONFLICT // http://stackoverflow.com/questions/942951/rest-api-error-return-good-practices // http://stackoverflow.com/questions/3825990/http-response-code-for-post-when-resource-already-exists // http://soabits.blogspot.com/2013/05/error-handling-considerations-and-best.html - // TODO: Some other error codes above should be changed to 400 from 404. And in other resources. Just sayin'. if ( $result instanceof WP_Error ) { return $result; } else { @@ -281,24 +301,28 @@ public function delete_user( $id, $force = false ) { $id = (int) $id; if ( empty( $id ) ) - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), + array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); // Permissions check if ( ! current_user_can( 'edit_users' ) ) { - return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => 401 ) ); + return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), + array( 'status' => HTTP_STATUS_FORBIDDEN ) ); } $user = get_userdata( $id ); if ( ! $user ) - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), + array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); // https://codex.wordpress.org/Function_Reference/wp_delete_user // TODO: Allow posts to be reassigned (see the docs for wp_delete_user) - use a HTTP parameter? $result = wp_delete_user( $id ); if ( ! $result ) - return new WP_Error( 'json_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 500 ) ); + return new WP_Error( 'json_cannot_delete', __( 'The user cannot be deleted.' ), + array( 'status' => HTTP_STATUS_INTERNAL_SERVER_ERROR ) ); } /** From 98375ac27a09d6ea8ae6f61c6b04ca2d881026a8 Mon Sep 17 00:00:00 2001 From: Toby Champion Date: Sat, 12 Apr 2014 18:46:46 -0700 Subject: [PATCH 013/515] Use absint() instead of (int) throughout --- lib/class-wp-json-users.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 953df70a6e..3c3fdcb225 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -90,7 +90,7 @@ public function get_users( $filter = array(), $context = 'view', $type = 'user', * @return response */ public function get_user( $id, $context = 'view' ) { - $id = (int) $id; + $id = absint( $id ); if ( ! current_user_can( 'edit_users' ) ) { return new WP_Error( 'json_cannot_get', __( 'Sorry, you are not allowed to get users.' ), @@ -172,7 +172,7 @@ protected function prepare_user( $user, $context = 'view' ) { * @return true on success */ function edit_user( $id, $data, $_headers = array() ) { - $id = (int) $id; + $id = absint( $id ); if ( empty( $id ) ) return new WP_Error( 'json_user_invalid_id', __( 'User ID must be supplied.' ), @@ -298,7 +298,7 @@ public function new_user( $data ) { * @return true on success */ public function delete_user( $id, $force = false ) { - $id = (int) $id; + $id = absint( $id ); if ( empty( $id ) ) return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), From 22fc0823d98b4be9af2fcc2ed21939a20aa83dc6 Mon Sep 17 00:00:00 2001 From: Toby Champion Date: Sat, 12 Apr 2014 18:53:07 -0700 Subject: [PATCH 014/515] Remove redundant $type parameter to get_users --- lib/class-wp-json-users.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 3c3fdcb225..c6de60fd6a 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -58,11 +58,10 @@ public function registerRoutes( $routes ) { * * @param array $filter optional * @param string $context optional - * @param string $type optional - * @param int $page optional + * @param int $page optional - TODO: Not implemented! * @return array contains a collection of User entities. */ - public function get_users( $filter = array(), $context = 'view', $type = 'user', $page = 1 ) { + public function get_users( $filter = array(), $context = 'view', $page = 1 ) { if ( ! current_user_can( 'edit_users' ) ) { return new WP_Error( 'json_cannot_get', __( 'Sorry, you are not allowed to get users.' ), From 1de6ea7197f4bf2107a273e08af959dcdce9971e Mon Sep 17 00:00:00 2001 From: Toby Champion Date: Sat, 12 Apr 2014 18:54:13 -0700 Subject: [PATCH 015/515] Remove unused Last-Modified code in get_user() --- lib/class-wp-json-users.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index c6de60fd6a..d749f8ebad 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -107,11 +107,7 @@ public function get_user( $id, $context = 'view' ) { return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); - // Link headers (see RFC 5988) - $response = new WP_JSON_Response(); - // user model doesn't appear to include a last-modified date - // $response->header( 'Last-Modified', mysql2date( 'D, d M Y H:i:s', $user->TBW ) . 'GMT' ); $user = $this->prepare_user( $user, $context ); if ( is_wp_error( $user ) ) From 3a33c085aaf05ee1ad7cb8ca857a828237c71a3a Mon Sep 17 00:00:00 2001 From: Toby Champion Date: Sat, 12 Apr 2014 18:59:26 -0700 Subject: [PATCH 016/515] Fix null response from DELETE (and do what Post endpoint does) --- lib/class-wp-json-users.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index d749f8ebad..b2b8bca6f2 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -1,6 +1,7 @@ HTTP_STATUS_INTERNAL_SERVER_ERROR ) ); + } else { + // "TODO: return a HTTP 202 here instead"... says the Post endpoint... really? Inappropriate (says tobych)? + return array( 'message' => __( 'Deleted user' ) ); + } } /** From fef2b7f150598681324a5f49509d7ffe00326b14 Mon Sep 17 00:00:00 2001 From: Toby Champion Date: Sat, 12 Apr 2014 19:11:59 -0700 Subject: [PATCH 017/515] Fix broken build (PHP 5.2.17) by moving const's into class --- lib/class-wp-json-users.php | 49 +++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index b2b8bca6f2..f8af90b651 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -1,15 +1,16 @@ HTTP_STATUS_FORBIDDEN ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } $args = array( 'orderby' => 'user_login', 'order' => 'ASC' ); @@ -94,19 +95,19 @@ public function get_user( $id, $context = 'view' ) { if ( ! current_user_can( 'edit_users' ) ) { return new WP_Error( 'json_cannot_get', __( 'Sorry, you are not allowed to get users.' ), - array( 'status' => HTTP_STATUS_FORBIDDEN ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } if ( empty( $id ) ) return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), - array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); // http://codex.wordpress.org/Function_Reference/get_userdata $user = get_userdata( $id ); if ( empty( $user->ID ) ) return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), - array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); $response = new WP_JSON_Response(); @@ -172,19 +173,19 @@ function edit_user( $id, $data, $_headers = array() ) { if ( empty( $id ) ) return new WP_Error( 'json_user_invalid_id', __( 'User ID must be supplied.' ), - array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); // http://codex.wordpress.org/Function_Reference/get_userdata $user = get_userdata( $id ); // returns False on failure if ( ! $user ) return new WP_Error( 'json_user_invalid_id', __( 'User ID must be an integer.' ), - array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); // Permissions check if ( ! current_user_can( 'edit_users' ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), - array( 'status' => HTTP_STATUS_FORBIDDEN ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } // Update attributes of the user from $data @@ -228,15 +229,15 @@ public function new_user( $data ) { if ( empty( $data['username'] )) { return new WP_Error( 'json_user_username_missing', __( 'No username supplied.'), - array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); } if ( empty( $data['password'] )) { return new WP_Error( 'json_user_password_missing', __( 'No password supplied.' ), - array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); } if ( empty( $data['email'] )) { return new WP_Error( 'json_user_email_missing', __( 'No email supplied.' ), - array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); } $userdata = array( @@ -298,19 +299,19 @@ public function delete_user( $id, $force = false ) { if ( empty( $id ) ) return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), - array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); // Permissions check if ( ! current_user_can( 'edit_users' ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), - array( 'status' => HTTP_STATUS_FORBIDDEN ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } $user = get_userdata( $id ); if ( ! $user ) return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), - array( 'status' => HTTP_STATUS_BAD_REQUEST ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); // https://codex.wordpress.org/Function_Reference/wp_delete_user // TODO: Allow posts to be reassigned (see the docs for wp_delete_user) - use a HTTP parameter? @@ -318,7 +319,7 @@ public function delete_user( $id, $force = false ) { if ( ! $result ) { return new WP_Error( 'json_cannot_delete', __( 'The user cannot be deleted.' ), - array( 'status' => HTTP_STATUS_INTERNAL_SERVER_ERROR ) ); + array( 'status' => HttpStatusCode::HTTP_STATUS_INTERNAL_SERVER_ERROR ) ); } else { // "TODO: return a HTTP 202 here instead"... says the Post endpoint... really? Inappropriate (says tobych)? return array( 'message' => __( 'Deleted user' ) ); From 5866f8f42ecb68737fcef80d1c5814af4415b62a Mon Sep 17 00:00:00 2001 From: Toby Champion Date: Sat, 12 Apr 2014 19:41:18 -0700 Subject: [PATCH 018/515] Check more appropriate capabilities, including meta caps Also, make error messages less apologetic. Sorry about that. --- lib/class-wp-json-users.php | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index f8af90b651..091d379cff 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -1,5 +1,6 @@ HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } @@ -93,8 +109,8 @@ public function get_users( $filter = array(), $context = 'view', $page = 1 ) { public function get_user( $id, $context = 'view' ) { $id = absint( $id ); - if ( ! current_user_can( 'edit_users' ) ) { - return new WP_Error( 'json_cannot_get', __( 'Sorry, you are not allowed to get users.' ), + if ( ! current_user_can( 'list_users' ) ) { + return new WP_Error( 'json_cannot_get', __( 'You need list_users capability to do this.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } @@ -183,8 +199,8 @@ function edit_user( $id, $data, $_headers = array() ) { array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); // Permissions check - if ( ! current_user_can( 'edit_users' ) ) { - return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), + if ( ! current_user_can( 'edit_user', $id ) ) { + return new WP_Error( 'json_cannot_edit', __( 'You need edit_user(s) capability to do this.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } @@ -302,8 +318,8 @@ public function delete_user( $id, $force = false ) { array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); // Permissions check - if ( ! current_user_can( 'edit_users' ) ) { - return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), + if ( ! current_user_can( 'delete_user', $id ) ) { + return new WP_Error( 'json_cannot_edit', __( 'You need delete_user(s) capability to do this' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } From 6bd08467d83e4cc7997e8a63584394b0e2f42a51 Mon Sep 17 00:00:00 2001 From: Kat Hagan Date: Sun, 13 Apr 2014 11:34:59 -0400 Subject: [PATCH 019/515] Typo fix :) --- docs/schema.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/schema.md b/docs/schema.md index 1b0d922074..a5693fc53e 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -8,7 +8,7 @@ This document is for clients and providers wanting to ensure full compliance with the specification. -Defintions +Definitions ========== The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be From c1c5ac8bbc074c2cf18fe1bde06145700cc3f60c Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 13 Apr 2014 16:01:14 -0500 Subject: [PATCH 020/515] Use correct JSON parameter names in compatibility docs. Corrected content parameter name and added parameter to demonstrate excerpt parameter. --- docs/compatibility.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index f51865baeb..3702028857 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -30,7 +30,7 @@ Content-Type: application/x-www-form-urlencoded Content-Type: application/json - {"post_title":"Hello World!","post_content":"Content"} + {"post_title":"Hello World!","content_raw":"Content","excerpt_raw":"Excerpt"} ### HTTP method compatibility From 985c87a9da11692b5ffa1f32f7ac09640eb80a3f Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 13 Apr 2014 16:04:59 -0500 Subject: [PATCH 021/515] Corrected title parameter name in compatibility docs. --- docs/compatibility.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index 3702028857..64202a335e 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -30,7 +30,7 @@ Content-Type: application/x-www-form-urlencoded Content-Type: application/json - {"post_title":"Hello World!","content_raw":"Content","excerpt_raw":"Excerpt"} + {"title":"Hello World!","content_raw":"Content","excerpt_raw":"Excerpt"} ### HTTP method compatibility From 1864468e2bfab572a3213f2c4faceffafab9b7c4 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 13 Apr 2014 16:09:49 -0500 Subject: [PATCH 022/515] Use correct JSON parameter names in post route docs. Corrected title and content parameter names and added parameter to demonstrate excerpts. --- docs/routes/posts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index 1c817e33f2..ca532b3cf7 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -60,7 +60,7 @@ Content-Type: application/x-www-form-urlencoded Content-Type: application/json - {"post_title":"Hello World!","post_content":"Content"} + {"title":"Hello World!","content_raw":"Content","excerpt_raw":"Excerpt"} ### Response On a successful creation, a 201 Created status is given, indicating that the @@ -123,4 +123,4 @@ that the post has been moved to the trash for permanent deletion at a later date. If force was set to true, a 200 OK status code will be returned instead, -indicating that the post has been permanently deleted. \ No newline at end of file +indicating that the post has been permanently deleted. From 72d48d1925e5223a388f2c42af14784f5059cdfc Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 15 Apr 2014 00:44:16 +1000 Subject: [PATCH 023/515] Check for valid JSON data before using --- lib/class-wp-json-server.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index e38428446f..e9a57ac5d9 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -372,7 +372,9 @@ public function dispatch() { } if ( $supported & self::ACCEPT_JSON ) { $data = json_decode( $this->get_raw_data(), true ); - $args = array_merge( $args, array( 'data' => $data ) ); + if ( $data !== null ) { + $args = array_merge( $args, array( 'data' => $data ) ); + } } elseif ( $supported & self::ACCEPT_RAW ) { $data = $this->get_raw_data(); From 744080c70458f0d4805209460abf98c55a3824fd Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 14 Apr 2014 10:07:33 -0500 Subject: [PATCH 024/515] Corrected parameter names in x-form examples. --- docs/compatibility.md | 2 +- docs/routes/posts.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index 64202a335e..05d31761d1 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -25,7 +25,7 @@ That is, the following are equivalent: Content-Type: application/x-www-form-urlencoded - data[post_title]=Hello%20World!&data[post_content]=Content + data[title]=Hello%20World!&data[content_raw]=Content&data[excerpt_raw]=Excerpt Content-Type: application/json diff --git a/docs/routes/posts.md b/docs/routes/posts.md index ca532b3cf7..84bcb5d68a 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -55,7 +55,7 @@ That is, the following are equivalent: Content-Type: application/x-www-form-urlencoded - data[post_title]=Hello%20World!&data[post_content]=Content + data[title]=Hello%20World!&data[content_raw]=Content&data[excerpt_raw]=Excerpt Content-Type: application/json From ccf5f1b8d015b1ef716d3838f1a47418ed75494e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 15 Apr 2014 10:53:58 +1000 Subject: [PATCH 025/515] Add authentication property to site index --- lib/class-wp-json-server.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index e9a57ac5d9..e8fe361f78 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -456,6 +456,7 @@ public function get_index() { 'description' => get_option( 'blogdescription' ), 'URL' => get_option( 'siteurl' ), 'routes' => array(), + 'authentication' => array(), 'meta' => array( 'links' => array( 'help' => 'https://github.com/rmccue/WP-API', From 11a2f942beeb8cbb50c5b70e6a15f0d404e0817b Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 15 Apr 2014 11:18:25 +1000 Subject: [PATCH 026/515] Include post ID with json_prepare_meta filter Props @kuching. Fixes #67. --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index e371210ea6..1e815021b9 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -632,7 +632,7 @@ protected function prepare_meta( $post_id ) { unset( $custom_fields[$meta_key] ); } - return apply_filters( 'json_prepare_meta', $custom_fields ); + return apply_filters( 'json_prepare_meta', $custom_fields, $post_id ); } protected function prepare_author( $author ) { From faf47548c96baf47259d57ca0f068ab0a6d6eeb4 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 15 Apr 2014 11:34:01 +1000 Subject: [PATCH 027/515] If pretty permalinks are disabled, use query URL --- plugin.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/plugin.php b/plugin.php index 783505e31a..8f722eb56c 100644 --- a/plugin.php +++ b/plugin.php @@ -229,10 +229,24 @@ function json_output_link_header() { * @return string Full URL to the endpoint */ function get_json_url( $blog_id = null, $path = '', $scheme = 'json' ) { - $url = get_home_url( $blog_id, 'wp-json', $scheme ); + if ( get_option( 'permalink_structure' ) ) { + $url = get_home_url( $blog_id, 'wp-json', $scheme ); - if ( !empty( $path ) && is_string( $path ) && strpos( $path, '..' ) === false ) - $url .= '/' . ltrim( $path, '/' ); + if ( !empty( $path ) && is_string( $path ) && strpos( $path, '..' ) === false ) + $url .= '/' . ltrim( $path, '/' ); + } + else { + $url = trailingslashit( get_home_url( $blog_id, '', $scheme ) ); + + if ( empty( $path ) ) { + $path = '/'; + } + else { + $path = '/' . ltrim( $path, '/' ); + } + + $url = add_query_arg( 'json_route', $path, $url ); + } return apply_filters( 'json_url', $url, $path, $blog_id ); } From 2be20589903a247d46115f6aac866899c6627518 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 15 Apr 2014 11:37:33 +1000 Subject: [PATCH 028/515] Pass the server instance to json_serve_request --- lib/class-wp-json-server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index e8fe361f78..1a3bf9ab1e 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -257,7 +257,7 @@ public function serve_request( $path = null ) { // This is a filter rather than an action, since this is designed to be // re-entrant if needed - $served = apply_filters( 'json_serve_request', false, $result, $path, $this->method ); + $served = apply_filters( 'json_serve_request', false, $result, $path, $this->method, $this ); if ( ! $served ) { if ( 'HEAD' === $this->method ) From f7927943a9b426c0a217b6a0e432f05049549047 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 15 Apr 2014 11:45:23 +1000 Subject: [PATCH 029/515] Add hook documentation --- lib/class-wp-json-server.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 1a3bf9ab1e..bbf0d9c553 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -255,8 +255,20 @@ public function serve_request( $path = null ) { $this->set_status( $code ); } - // This is a filter rather than an action, since this is designed to be - // re-entrant if needed + /** + * Allow sending the request manually + * + * If `$served` is true, the result will not be sent to the client. + * + * This is a filter rather than an action, since this is designed to be + * re-entrant if needed. + * + * @param bool $served Whether the request has already been served + * @param mixed $result Result to send to the client. JsonSerializable, or other value to pass to `json_encode` + * @param string $path Route requested + * @param string $method HTTP request method (HEAD/GET/POST/PUT/PATCH/DELETE) + * @param WP_JSON_ResponseHandler $this ResponseHandler instance (usually WP_JSON_Server) + */ $served = apply_filters( 'json_serve_request', false, $result, $path, $this->method, $this ); if ( ! $served ) { From c1046dcd3306419beb20c2ce95773ac944b635b5 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 15 Apr 2014 11:50:47 +1000 Subject: [PATCH 030/515] Only show post meta on edit context See http://irclogs.wordpress.org/chanlog.php?channel=wordpress-dev&day=2014-04-15&sort=asc#m832939 for the discussion around this. This is a temporary workaround for the data privacy problem and still requires a more permanent fix. --- lib/class-wp-json-posts.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index e371210ea6..6f22211af8 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -524,6 +524,7 @@ protected function prepare_post( $post, $context = 'view' ) { 'title_raw' => $post['post_title'], 'content_raw' => $post['post_content'], 'guid_raw' => $post['guid'], + 'post_meta' => $this->prepare_meta( $post['ID'] ), ); // Dates @@ -576,9 +577,6 @@ protected function prepare_post( $post, $context = 'view' ) { elseif ( 'edit' === $context ) return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); - // Post meta - $_post['post_meta'] = $this->prepare_meta( $post['ID'] ); - // Entity meta $_post['meta'] = array( 'links' => array( From c19a15270a50c9ad0daaf039aa041f59f7a20c77 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 20 Apr 2014 21:02:13 +1000 Subject: [PATCH 031/515] Add show_in_json argument to register_post_type --- lib/class-wp-json-posts.php | 24 ++++++++++++++++++++---- plugin.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 1e815021b9..e4144fd801 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -81,7 +81,7 @@ public function get_posts( $filter = array(), $context = 'view', $type = 'post', $query = array(); $post_type = get_post_type_object( $type ); - if ( ! ( (bool) $post_type ) ) + if ( ! ( (bool) $post_type ) || ! $post_type->show_in_json ) return new WP_Error( 'json_invalid_post_type', __( 'The post type specified is not valid' ), array( 'status' => 403 ) ); $query['post_type'] = $post_type->name; @@ -159,8 +159,14 @@ public function get_posts( $filter = array(), $context = 'view', $type = 'post', * @return boolean Can we read it? */ protected function check_read_permission( $post ) { - // Can we read the post? $post_type = get_post_type_object( $post['post_type'] ); + + // Ensure the post type can be read + if ( ! $post_type->show_in_json ) { + return false; + } + + // Can we read the post? if ( 'publish' === $post['post_status'] || current_user_can( $post_type->cap->read_post, $post['ID'] ) ) { return true; } @@ -243,7 +249,6 @@ public function get_post( $id, $context = 'view' ) { if ( empty( $post['ID'] ) ) return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - $post_type = get_post_type_object( $post['post_type'] ); if ( ! $this->check_read_permission( $post ) ) return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); @@ -363,6 +368,16 @@ public function get_comments( $id ) { //$args = array('status' => $status, 'post_id' => $id, 'offset' => $offset, 'number' => $number )l $comments = get_comments( array('post_id' => $id) ); + $post = get_post( $id, ARRAY_A ); + + if ( empty( $post['ID'] ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + if ( ! $this->check_read_permission( $post ) ) { + return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); + } + $struct = array(); foreach ( $comments as $comment ) { $struct[] = $this->prepare_comment( $comment, array( 'comment', 'meta' ), 'collection' ); @@ -414,8 +429,9 @@ public function get_post_type( $type, $_in_collection = false ) { if ( ! is_object( $type ) ) $type = get_post_type_object($type); - if ( $type->public === false ) + if ( $type->show_in_json === false ) { return new WP_Error( 'json_cannot_read_type', __( 'Cannot view post type' ), array( 'status' => 403 ) ); + } $data = array( 'name' => $type->label, diff --git a/plugin.php b/plugin.php index 783505e31a..63949a7568 100644 --- a/plugin.php +++ b/plugin.php @@ -219,6 +219,36 @@ function json_output_link_header() { } add_action( 'template_redirect', 'json_output_link_header', 11, 0 ); +/** + * Add `show_in_json` {@see register_post_type} argument + * + * Adds the `show_in_json` post type argument to {@see register_post_type}. This + * value controls whether the post type is available via API endpoints, and + * defaults to the value of `publicly_queryable` + * + * @param string $post_type Post type being registered + * @param stdClass $args Post type arguments + */ +function json_register_post_type( $post_type, $args ) { + global $wp_post_types; + + $type = &$wp_post_types[ $post_type ]; + + if ( $post_type === 'post' ) { + $type->show_in_json = false; + } + + // Exception for pages + if ( $post_type === 'page' ) { + $type->show_in_json = true; + } + + if ( ! isset( $type->show_in_json ) ) { + $type->show_in_json = $type->publicly_queryable; + } +} +add_action( 'registered_post_type', 'json_register_post_type', 10, 2 ); + /** * Get URL to a JSON endpoint on a site * From fad81d45df487abd5c50a4b0979c736013c03ad7 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:10:04 +1000 Subject: [PATCH 032/515] Reformat some control statements and blocks --- lib/class-wp-json-users.php | 80 ++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 091d379cff..b1a0a65f6f 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -82,18 +82,22 @@ public function registerRoutes( $routes ) { public function get_users( $filter = array(), $context = 'view', $page = 1 ) { if ( ! current_user_can( 'list_users' ) ) { - return new WP_Error( 'json_cannot_get', __( 'You need list_users capability to do this.' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + return new WP_Error( 'json_cannot_get', __( 'You need list_users capability to do this.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } - $args = array( 'orderby' => 'user_login', 'order' => 'ASC' ); + $args = array( + 'orderby' => 'user_login', + 'order' => 'ASC' + ); + $user_query = new WP_User_Query( $args ); $struct = array(); if ( ! empty( $user_query->results ) ) { foreach ( $user_query->results as $user ) { $struct[] = $this->prepare_user( $user, $context ); } - } else { + } + else { return array(); } return $struct; @@ -114,22 +118,23 @@ public function get_user( $id, $context = 'view' ) { array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } - if ( empty( $id ) ) - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + if ( empty( $id ) ) { + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + } // http://codex.wordpress.org/Function_Reference/get_userdata $user = get_userdata( $id ); - if ( empty( $user->ID ) ) - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + if ( empty( $user->ID ) ) { + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + } $response = new WP_JSON_Response(); $user = $this->prepare_user( $user, $context ); - if ( is_wp_error( $user ) ) + if ( is_wp_error( $user ) ) { return $user; + } $response->set_data( $user ); return $response; @@ -187,21 +192,20 @@ protected function prepare_user( $user, $context = 'view' ) { function edit_user( $id, $data, $_headers = array() ) { $id = absint( $id ); - if ( empty( $id ) ) - return new WP_Error( 'json_user_invalid_id', __( 'User ID must be supplied.' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + if ( empty( $id ) ) { + return new WP_Error( 'json_user_invalid_id', __( 'User ID must be supplied.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + } // http://codex.wordpress.org/Function_Reference/get_userdata $user = get_userdata( $id ); // returns False on failure - if ( ! $user ) - return new WP_Error( 'json_user_invalid_id', __( 'User ID must be an integer.' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + if ( ! $user ) { + return new WP_Error( 'json_user_invalid_id', __( 'User ID must be an integer.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + } // Permissions check if ( ! current_user_can( 'edit_user', $id ) ) { - return new WP_Error( 'json_cannot_edit', __( 'You need edit_user(s) capability to do this.' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + return new WP_Error( 'json_cannot_edit', __( 'You need edit_user(s) capability to do this.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } // Update attributes of the user from $data @@ -243,17 +247,14 @@ public function new_user( $data ) { # https://codex.wordpress.org/Class_Reference/WP_User # http://tommcfarlin.com/create-a-user-in-wordpress/ - if ( empty( $data['username'] )) { - return new WP_Error( 'json_user_username_missing', __( 'No username supplied.'), - array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + if ( empty( $data['username'] ) ) { + return new WP_Error( 'json_user_username_missing', __( 'No username supplied.'), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); } - if ( empty( $data['password'] )) { - return new WP_Error( 'json_user_password_missing', __( 'No password supplied.' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + if ( empty( $data['password'] ) ) { + return new WP_Error( 'json_user_password_missing', __( 'No password supplied.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); } - if ( empty( $data['email'] )) { - return new WP_Error( 'json_user_email_missing', __( 'No email supplied.' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + if ( empty( $data['email'] ) ) { + return new WP_Error( 'json_user_email_missing', __( 'No email supplied.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); } $userdata = array( @@ -313,30 +314,29 @@ public function new_user( $data ) { public function delete_user( $id, $force = false ) { $id = absint( $id ); - if ( empty( $id ) ) - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + if ( empty( $id ) ) { + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + } // Permissions check if ( ! current_user_can( 'delete_user', $id ) ) { - return new WP_Error( 'json_cannot_edit', __( 'You need delete_user(s) capability to do this' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + return new WP_Error( 'json_cannot_edit', __( 'You need delete_user(s) capability to do this' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } $user = get_userdata( $id ); - if ( ! $user ) - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + if ( ! $user ) { + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + } // https://codex.wordpress.org/Function_Reference/wp_delete_user // TODO: Allow posts to be reassigned (see the docs for wp_delete_user) - use a HTTP parameter? $result = wp_delete_user( $id ); if ( ! $result ) { - return new WP_Error( 'json_cannot_delete', __( 'The user cannot be deleted.' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_INTERNAL_SERVER_ERROR ) ); - } else { + return new WP_Error( 'json_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_INTERNAL_SERVER_ERROR ) ); + } + else { // "TODO: return a HTTP 202 here instead"... says the Post endpoint... really? Inappropriate (says tobych)? return array( 'message' => __( 'Deleted user' ) ); } @@ -380,6 +380,6 @@ protected function update_user( $user, $data ) { } // ignore registered - probably no need - } + } } From 724603e9cbea63ce27aa1346b959d3544c1cb966 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:11:27 +1000 Subject: [PATCH 033/515] registerRoutes -> register_routes --- lib/class-wp-json-users.php | 2 +- plugin.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index b1a0a65f6f..4401a99781 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -51,7 +51,7 @@ public function __construct( WP_JSON_ResponseHandler $server ) { * @param array $routes Existing routes * @return array Modified routes */ - public function registerRoutes( $routes ) { + public function register_routes( $routes ) { $user_routes = array( // User endpoints '/users' => array( diff --git a/plugin.php b/plugin.php index 03925453d3..052ad0d0c4 100644 --- a/plugin.php +++ b/plugin.php @@ -52,7 +52,7 @@ function json_api_default_filters($server) { // Users $wp_json_users = new WP_JSON_Users($server); - add_filter( 'json_endpoints', array( $wp_json_users, 'registerRoutes' ), 0 ); + add_filter( 'json_endpoints', array( $wp_json_users, 'register_routes' ), 0 ); // Pages $wp_json_pages = new WP_JSON_Pages($server); From 8d3b6efac98b881b0932372bd744e3facd16f8bd Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:14:01 +1000 Subject: [PATCH 034/515] Use consistent error messages This brings permissions error messages in line with the post endpoints. --- lib/class-wp-json-users.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 4401a99781..a14bb3bb16 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -82,7 +82,7 @@ public function register_routes( $routes ) { public function get_users( $filter = array(), $context = 'view', $page = 1 ) { if ( ! current_user_can( 'list_users' ) ) { - return new WP_Error( 'json_cannot_get', __( 'You need list_users capability to do this.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + return new WP_Error( 'json_user_cannot_list', __( 'Sorry, you are not allowed to list users.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } $args = array( @@ -114,8 +114,7 @@ public function get_user( $id, $context = 'view' ) { $id = absint( $id ); if ( ! current_user_can( 'list_users' ) ) { - return new WP_Error( 'json_cannot_get', __( 'You need list_users capability to do this.' ), - array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + return new WP_Error( 'json_user_cannot_list', __( 'Sorry, you are not allowed to view this user.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } if ( empty( $id ) ) { @@ -205,7 +204,7 @@ function edit_user( $id, $data, $_headers = array() ) { // Permissions check if ( ! current_user_can( 'edit_user', $id ) ) { - return new WP_Error( 'json_cannot_edit', __( 'You need edit_user(s) capability to do this.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + return new WP_Error( 'json_user_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } // Update attributes of the user from $data @@ -320,7 +319,7 @@ public function delete_user( $id, $force = false ) { // Permissions check if ( ! current_user_can( 'delete_user', $id ) ) { - return new WP_Error( 'json_cannot_edit', __( 'You need delete_user(s) capability to do this' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + return new WP_Error( 'json_user_cannot_delete', __( 'Sorry, you are not allowed to delete this user.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } $user = get_userdata( $id ); From d59579ff52c03919c123f82b86be8174e29f4a2a Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:19:29 +1000 Subject: [PATCH 035/515] Check permissions on user creation --- lib/class-wp-json-users.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index a14bb3bb16..0424edf086 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -246,6 +246,10 @@ public function new_user( $data ) { # https://codex.wordpress.org/Class_Reference/WP_User # http://tommcfarlin.com/create-a-user-in-wordpress/ + if ( ! current_user_can( 'create_users' ) ) { + return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to create users.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + } + if ( empty( $data['username'] ) ) { return new WP_Error( 'json_user_username_missing', __( 'No username supplied.'), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); } From 7b9b2a62ef11f7b0358e02f4614d5b65bb6033b1 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:25:45 +1000 Subject: [PATCH 036/515] Rework some control flows --- lib/class-wp-json-users.php | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 0424edf086..93b6ff1c5e 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -91,15 +91,15 @@ public function get_users( $filter = array(), $context = 'view', $page = 1 ) { ); $user_query = new WP_User_Query( $args ); - $struct = array(); - if ( ! empty( $user_query->results ) ) { - foreach ( $user_query->results as $user ) { - $struct[] = $this->prepare_user( $user, $context ); - } - } - else { + if ( empty( $user_query->results ) ) { return array(); } + + $struct = array(); + foreach ( $user_query->results as $user ) { + $struct[] = $this->prepare_user( $user, $context ); + } + return $struct; } @@ -128,15 +128,7 @@ public function get_user( $id, $context = 'view' ) { return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); } - $response = new WP_JSON_Response(); - - $user = $this->prepare_user( $user, $context ); - if ( is_wp_error( $user ) ) { - return $user; - } - - $response->set_data( $user ); - return $response; + return $this->prepare_user( $user, $context ); } /** @@ -280,12 +272,12 @@ public function new_user( $data ) { // Pre-insert hook // TODO: Or json_pre_insert_user? Or insert rather than create? "Insert" seems to mean create or edit in WP...? - $can_insert = apply_filters( 'pre_wp_json_create_user', $userdata, $data ); + $can_insert = apply_filters( 'json_pre_create_user', $userdata, $data ); if ( is_wp_error( $can_insert ) ) { return $can_insert; } - $result = wp_insert_user( $userdata ); + $user_id = wp_insert_user( $userdata ); // TODO: Send appropriate HTTP error codes along with the JSON rendering of the WP_Error we send back // TODO: I guess we can just add/overwrite the 'status' code in there ourselves... nested WP_Error? // These are the errors wp_insert_user() might return (from the wp_create_user documentation) @@ -295,10 +287,8 @@ public function new_user( $data ) { // http://stackoverflow.com/questions/942951/rest-api-error-return-good-practices // http://stackoverflow.com/questions/3825990/http-response-code-for-post-when-resource-already-exists // http://soabits.blogspot.com/2013/05/error-handling-considerations-and-best.html - if ( $result instanceof WP_Error ) { - return $result; - } else { - $user_id = $result; + if ( is_wp_error( $user_id ) ) { + return $user_id; } $response = $this->get_user( $user_id ); From 6de2a8410000ff025350571ceac789840a2bd887 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:26:29 +1000 Subject: [PATCH 037/515] Remove some redundant comments --- lib/class-wp-json-users.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 93b6ff1c5e..c4c7d56667 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -121,7 +121,6 @@ public function get_user( $id, $context = 'view' ) { return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); } - // http://codex.wordpress.org/Function_Reference/get_userdata $user = get_userdata( $id ); if ( empty( $user->ID ) ) { @@ -215,7 +214,6 @@ function edit_user( $id, $data, $_headers = array() ) { return $retval; } - // http://codex.wordpress.org/Function_Reference/do_action do_action( 'json_insert_user', $user, $data, true ); // $update is always true return $this->get_user( $id ); From 616f24a135c06de7a8a2d5c4d999841260f98356 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:26:53 +1000 Subject: [PATCH 038/515] Use the result of json_pre_insert_data --- lib/class-wp-json-users.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index c4c7d56667..667d3275cf 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -270,9 +270,9 @@ public function new_user( $data ) { // Pre-insert hook // TODO: Or json_pre_insert_user? Or insert rather than create? "Insert" seems to mean create or edit in WP...? - $can_insert = apply_filters( 'json_pre_create_user', $userdata, $data ); - if ( is_wp_error( $can_insert ) ) { - return $can_insert; + $userdata = apply_filters( 'json_pre_create_user', $userdata, $data ); + if ( is_wp_error( $userdata ) ) { + return $userdata; } $user_id = wp_insert_user( $userdata ); From 56d45fd649a50adaf1979384618fa6d8259a67ed Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:28:18 +1000 Subject: [PATCH 039/515] Correct hook name --- lib/class-wp-json-users.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 667d3275cf..b2ba9365c8 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -205,7 +205,7 @@ function edit_user( $id, $data, $_headers = array() ) { } // Pre-update hook - $user = apply_filters( 'pre_wp_json_update_user', $user, $id, $data, $_headers ); + $user = apply_filters( 'json_pre_update_user', $user, $id, $data, $_headers ); // Update the user in the database // http://codex.wordpress.org/Function_Reference/wp_update_user From cc96ba46947487215983ae8a20a992bf7ddabb44 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:31:05 +1000 Subject: [PATCH 040/515] Add json_prepare_user filter --- lib/class-wp-json-users.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index b2ba9365c8..b92abb8c61 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -165,7 +165,7 @@ protected function prepare_user( $user, $context = 'view' ) { ), ), ); - return $user_fields; + return apply_filters( 'json_prepare_user', $user_fields, $user, $context ); } /** From 398cd53b3739fe4754e571ffff50e12b877b87c7 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:32:05 +1000 Subject: [PATCH 041/515] Don't double-check user ID get_userdata will check if the user ID is valid, let's not second guess this ourselves. --- lib/class-wp-json-users.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index b92abb8c61..ca3e857ba0 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -111,16 +111,10 @@ public function get_users( $filter = array(), $context = 'view', $page = 1 ) { * @return response */ public function get_user( $id, $context = 'view' ) { - $id = absint( $id ); - if ( ! current_user_can( 'list_users' ) ) { return new WP_Error( 'json_user_cannot_list', __( 'Sorry, you are not allowed to view this user.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } - if ( empty( $id ) ) { - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); - } - $user = get_userdata( $id ); if ( empty( $user->ID ) ) { From dcc8f144f74d132d8b97566479aef686dd800123 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:40:04 +1000 Subject: [PATCH 042/515] Pass get_users $filter/$page into WP_User_Query() --- lib/class-wp-json-users.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index ca3e857ba0..d920b70b4f 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -70,13 +70,9 @@ public function register_routes( $routes ) { /** * Retrieve users. * - * The optional $filter parameter... TODO: Not implemented! - * Accepted keys are... TODO: Not implemented! - * The optional $fields parameter... TODO: Not implemented! - * - * @param array $filter optional + * @param array $filter Extra query parameters for {@see WP_User_Query} * @param string $context optional - * @param int $page optional - TODO: Not implemented! + * @param int $page Page number (1-indexed) * @return array contains a collection of User entities. */ public function get_users( $filter = array(), $context = 'view', $page = 1 ) { @@ -89,6 +85,13 @@ public function get_users( $filter = array(), $context = 'view', $page = 1 ) { 'orderby' => 'user_login', 'order' => 'ASC' ); + $args = array_merge( $args, $filter ); + $args = apply_filters( 'json_user_query', $args, $filter, $context, $page ); + + // Pagination + $args['number'] = empty( $args['number'] ) ? 10 : absint( $args['number'] ); + $page = absint( $page ); + $args['offset'] = ( $page - 1 ) * $args['number']; $user_query = new WP_User_Query( $args ); if ( empty( $user_query->results ) ) { From 7d5c3919e6bfd443f37a6ca87edf0356aba70ce0 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:53:28 +1000 Subject: [PATCH 043/515] Remove excess comments --- lib/class-wp-json-users.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index d920b70b4f..f2f58d684b 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -136,25 +136,15 @@ public function get_user( $id, $context = 'view' ) { * @return array */ protected function prepare_user( $user, $context = 'view' ) { - // We're ignoring $fields for now, so you get all these fields - // http://codex.wordpress.org/Function_Reference/get_metadata - // http://code.tutsplus.com/articles/mastering-wordpress-meta-data-understanding-and-using-arrays--wp-34596 $user_fields = array( - - // As per https://github.com/WP-API/WP-API/blob/master/docs/schema.md#user 'ID' => $user->ID, 'name' => $user->display_name, 'slug' => $user->user_nicename, - 'URL' => $user->user_url, // TODO: this is called 'Website' in the Wordpress users page. Use that? - // Read-only/derived + 'URL' => $user->user_url, 'avatar' => $this->server->get_avatar_url( $user->user_email ), - - // Extra stuff that seems important and isn't in user_meta 'username' => $user->user_login, 'email' => $user->user_email, 'registered' => $user->user_registered, - - // TODO: There's also user_activation_key, user_status, password... roles 'meta' => array( 'links' => array( 'self' => json_url( '/users/' . $user->ID ), From 99a6e283f383d2b4c132c943476c323c47653918 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:55:01 +1000 Subject: [PATCH 044/515] Add caps/roles to data returned from prepare_user --- lib/class-wp-json-users.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index f2f58d684b..555461cf2a 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -145,6 +145,8 @@ protected function prepare_user( $user, $context = 'view' ) { 'username' => $user->user_login, 'email' => $user->user_email, 'registered' => $user->user_registered, + 'roles' => $user->roles, + 'capabilities' => $user->allcaps, 'meta' => array( 'links' => array( 'self' => json_url( '/users/' . $user->ID ), @@ -152,6 +154,12 @@ protected function prepare_user( $user, $context = 'view' ) { ), ), ); + + if ( $context === 'edit' ) { + // The user's specific caps should only be needed if you're editing + // the user, as allcaps should handle most users + $user_fields['extra_capabilities'] = $user->caps; + } return apply_filters( 'json_prepare_user', $user_fields, $user, $context ); } From 0b92ba1c462d27d9b7866eaefb1d4b9e59fef328 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 00:59:34 +1000 Subject: [PATCH 045/515] Add extra name fields to prepare_user --- lib/class-wp-json-users.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 555461cf2a..cd803a151c 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -138,11 +138,14 @@ public function get_user( $id, $context = 'view' ) { protected function prepare_user( $user, $context = 'view' ) { $user_fields = array( 'ID' => $user->ID, + 'username' => $user->user_login, 'name' => $user->display_name, + 'first_name' => $user->first_name, + 'last_name' => $user->last_name, + 'nickname' => $user->nickname, 'slug' => $user->user_nicename, 'URL' => $user->user_url, 'avatar' => $this->server->get_avatar_url( $user->user_email ), - 'username' => $user->user_login, 'email' => $user->user_email, 'registered' => $user->user_registered, 'roles' => $user->roles, From 299cea344f9b56587d1d4b81f50456f3501ecb24 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 01:00:25 +1000 Subject: [PATCH 046/515] Add user description to prepare_user --- lib/class-wp-json-users.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index cd803a151c..5a7cd2f8fb 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -146,6 +146,7 @@ protected function prepare_user( $user, $context = 'view' ) { 'slug' => $user->user_nicename, 'URL' => $user->user_url, 'avatar' => $this->server->get_avatar_url( $user->user_email ), + 'description' => $user->description, 'email' => $user->user_email, 'registered' => $user->user_registered, 'roles' => $user->roles, From e32e6649c6dc61e6f25107b87fdf1170c25aed7f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 01:02:31 +1000 Subject: [PATCH 047/515] Ensure meta is always last --- lib/class-wp-json-users.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 5a7cd2f8fb..40f21b8ee4 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -151,12 +151,6 @@ protected function prepare_user( $user, $context = 'view' ) { 'registered' => $user->user_registered, 'roles' => $user->roles, 'capabilities' => $user->allcaps, - 'meta' => array( - 'links' => array( - 'self' => json_url( '/users/' . $user->ID ), - 'archives' => json_url( '/users/' . $user->ID . '/posts' ), - ), - ), ); if ( $context === 'edit' ) { @@ -164,6 +158,13 @@ protected function prepare_user( $user, $context = 'view' ) { // the user, as allcaps should handle most users $user_fields['extra_capabilities'] = $user->caps; } + + $user_fields['meta'] = array( + 'links' => array( + 'self' => json_url( '/users/' . $user->ID ), + 'archives' => json_url( '/users/' . $user->ID . '/posts' ), + ), + ); return apply_filters( 'json_prepare_user', $user_fields, $user, $context ); } From 80db2b58fa73abd7e85b3ef13c3ed798b4d76390 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 01:05:23 +1000 Subject: [PATCH 048/515] Don't reveal whether users exist before checking permissions --- lib/class-wp-json-users.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 40f21b8ee4..db1379fdf3 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -186,18 +186,16 @@ function edit_user( $id, $data, $_headers = array() ) { return new WP_Error( 'json_user_invalid_id', __( 'User ID must be supplied.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); } - // http://codex.wordpress.org/Function_Reference/get_userdata - $user = get_userdata( $id ); // returns False on failure - - if ( ! $user ) { - return new WP_Error( 'json_user_invalid_id', __( 'User ID must be an integer.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); - } - // Permissions check if ( ! current_user_can( 'edit_user', $id ) ) { return new WP_Error( 'json_user_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); } + $user = get_userdata( $id ); + if ( ! $user ) { + return new WP_Error( 'json_user_invalid_id', __( 'User ID is invalid.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + } + // Update attributes of the user from $data $retval = $this->update_user( $user, $data ); if ( is_wp_error( $retval ) ) { From d69b9838a3a57e7d995fc3dc53dcb16a8b65d177 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 01:05:52 +1000 Subject: [PATCH 049/515] Ensure all methods have visibility specified --- lib/class-wp-json-users.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index db1379fdf3..b9517057c9 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -179,7 +179,7 @@ protected function prepare_user( $user, $context = 'view' ) { * @param array $_headers Header data * @return true on success */ - function edit_user( $id, $data, $_headers = array() ) { + public function edit_user( $id, $data, $_headers = array() ) { $id = absint( $id ); if ( empty( $id ) ) { From 034d1cae8a4b964b0862b2920879eded1ceee132 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 01:07:39 +1000 Subject: [PATCH 050/515] Remove HttpStatusCode class (enum) I like the concept, but it doesn't belong in this PR. --- lib/class-wp-json-users.php | 41 +++++++++++++------------------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index b9517057c9..051415170a 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -1,18 +1,5 @@ HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + return new WP_Error( 'json_user_cannot_list', __( 'Sorry, you are not allowed to list users.' ), array( 'status' => 403 ) ); } $args = array( @@ -115,13 +102,13 @@ public function get_users( $filter = array(), $context = 'view', $page = 1 ) { */ public function get_user( $id, $context = 'view' ) { if ( ! current_user_can( 'list_users' ) ) { - return new WP_Error( 'json_user_cannot_list', __( 'Sorry, you are not allowed to view this user.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + return new WP_Error( 'json_user_cannot_list', __( 'Sorry, you are not allowed to view this user.' ), array( 'status' => 403 ) ); } $user = get_userdata( $id ); if ( empty( $user->ID ) ) { - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 400 ) ); } return $this->prepare_user( $user, $context ); @@ -183,17 +170,17 @@ public function edit_user( $id, $data, $_headers = array() ) { $id = absint( $id ); if ( empty( $id ) ) { - return new WP_Error( 'json_user_invalid_id', __( 'User ID must be supplied.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + return new WP_Error( 'json_user_invalid_id', __( 'User ID must be supplied.' ), array( 'status' => 400 ) ); } // Permissions check if ( ! current_user_can( 'edit_user', $id ) ) { - return new WP_Error( 'json_user_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + return new WP_Error( 'json_user_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => 403 ) ); } $user = get_userdata( $id ); if ( ! $user ) { - return new WP_Error( 'json_user_invalid_id', __( 'User ID is invalid.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + return new WP_Error( 'json_user_invalid_id', __( 'User ID is invalid.' ), array( 'status' => 400 ) ); } // Update attributes of the user from $data @@ -235,17 +222,17 @@ public function new_user( $data ) { # http://tommcfarlin.com/create-a-user-in-wordpress/ if ( ! current_user_can( 'create_users' ) ) { - return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to create users.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to create users.' ), array( 'status' => 403 ) ); } if ( empty( $data['username'] ) ) { - return new WP_Error( 'json_user_username_missing', __( 'No username supplied.'), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + return new WP_Error( 'json_user_username_missing', __( 'No username supplied.'), array( 'status' => 400 ) ); } if ( empty( $data['password'] ) ) { - return new WP_Error( 'json_user_password_missing', __( 'No password supplied.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + return new WP_Error( 'json_user_password_missing', __( 'No password supplied.' ), array( 'status' => 400 ) ); } if ( empty( $data['email'] ) ) { - return new WP_Error( 'json_user_email_missing', __( 'No email supplied.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + return new WP_Error( 'json_user_email_missing', __( 'No email supplied.' ), array( 'status' => 400 ) ); } $userdata = array( @@ -304,18 +291,18 @@ public function delete_user( $id, $force = false ) { $id = absint( $id ); if ( empty( $id ) ) { - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 400 ) ); } // Permissions check if ( ! current_user_can( 'delete_user', $id ) ) { - return new WP_Error( 'json_user_cannot_delete', __( 'Sorry, you are not allowed to delete this user.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_FORBIDDEN ) ); + return new WP_Error( 'json_user_cannot_delete', __( 'Sorry, you are not allowed to delete this user.' ), array( 'status' => 403 ) ); } $user = get_userdata( $id ); if ( ! $user ) { - return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_BAD_REQUEST ) ); + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 400 ) ); } // https://codex.wordpress.org/Function_Reference/wp_delete_user @@ -323,7 +310,7 @@ public function delete_user( $id, $force = false ) { $result = wp_delete_user( $id ); if ( ! $result ) { - return new WP_Error( 'json_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => HttpStatusCode::HTTP_STATUS_INTERNAL_SERVER_ERROR ) ); + return new WP_Error( 'json_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 500 ) ); } else { // "TODO: return a HTTP 202 here instead"... says the Post endpoint... really? Inappropriate (says tobych)? From 59781672ee230509347923c34f6f2240c946a4f0 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 21 Apr 2014 01:14:47 +1000 Subject: [PATCH 051/515] Remove unneeded comment See https://github.com/WP-API/WP-API/pull/146/files#r11797963 for comment regarding this. --- lib/class-wp-json-users.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 051415170a..7d2e65a4f2 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -313,7 +313,6 @@ public function delete_user( $id, $force = false ) { return new WP_Error( 'json_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 500 ) ); } else { - // "TODO: return a HTTP 202 here instead"... says the Post endpoint... really? Inappropriate (says tobych)? return array( 'message' => __( 'Deleted user' ) ); } } From bd2ab2879b8a49ccaa54d875d35d383bfda7c9c4 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Mon, 21 Apr 2014 19:22:33 -0700 Subject: [PATCH 052/515] Add the post id to the $post array when creating a new post, so anything hooked into the actions have this available --- lib/class-wp-json-posts.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 1e815021b9..45b2457a44 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -863,6 +863,11 @@ protected function insert_post( $data ) { return $post_ID; } + // If this is a new post, add the post ID to $post + if ( ! $update ) { + $post['ID'] = $post_ID; + } + // Sticky if ( isset( $post['sticky'] ) ) { if ( $post['sticky'] ) From fd29d956c879e9ff292fc20b97a4688431a653c6 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 23 Apr 2014 21:09:19 -0500 Subject: [PATCH 053/515] Use version option to check on init if rewrite rules should be flushed #113. --- plugin.php | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/plugin.php b/plugin.php index 783505e31a..131d005fa9 100644 --- a/plugin.php +++ b/plugin.php @@ -7,6 +7,17 @@ * Version: 0.9 * Plugin URI: https://github.com/rmccue/WP-API */ + +/** + * Version number for our API + * + * @var string + */ +define('JSON_API_VERSION', '0.9'); + +/** + * Include our files for the API + */ include_once( dirname( __FILE__ ) . '/lib/class-jsonserializable.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-datetime.php' ); @@ -37,6 +48,20 @@ function json_api_register_rewrites() { add_rewrite_rule( '^wp-json(.*)?','index.php?json_route=$matches[1]','top' ); } +/** + * Determine if the rewrite rules should be flushed. + */ +function json_api_maybe_flush_rewrites() { + $version = get_option( 'json_api_plugin_version', NULL ); + + if ( empty( $version ) || $version !== JSON_API_VERSION ) { + flush_rewrite_rules(); + update_option( 'json_api_plugin_version', JSON_API_VERSION ); + } + +} +add_action( 'init', 'json_api_maybe_flush_rewrites', 999 ); + /** * Register the default JSON API filters * @@ -135,7 +160,7 @@ function json_api_activation( $network_wide ) { switch_to_blog( $mu_blog['blog_id'] ); json_api_register_rewrites(); - flush_rewrite_rules(); + update_option( 'json_api_plugin_version', NULL ); } restore_current_blog(); @@ -143,7 +168,7 @@ function json_api_activation( $network_wide ) { } else { json_api_register_rewrites(); - flush_rewrite_rules(); + update_option( 'json_api_plugin_version', NULL ); } } register_activation_hook( __FILE__, 'json_api_activation' ); @@ -159,14 +184,14 @@ function json_api_deactivation( $network_wide ) { foreach ( $mu_blogs as $mu_blog ) { switch_to_blog( $mu_blog['blog_id'] ); - flush_rewrite_rules(); + update_option( 'json_api_plugin_version', NULL ); } restore_current_blog(); } else { - flush_rewrite_rules(); + update_option( 'json_api_plugin_version', NULL ); } } register_deactivation_hook( __FILE__, 'json_api_deactivation' ); From 187779ee6bbd2541b26d3b3dd27cc08f0314925f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 25 Apr 2014 10:02:10 +1000 Subject: [PATCH 054/515] Unify insertion/update code --- lib/class-wp-json-users.php | 197 ++++++++++++++++++------------------ 1 file changed, 99 insertions(+), 98 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 7d2e65a4f2..305defb86d 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -155,6 +155,99 @@ protected function prepare_user( $user, $context = 'view' ) { return apply_filters( 'json_prepare_user', $user_fields, $user, $context ); } + protected function insert_user( $data ) { + if ( ! empty( $data['ID'] ) ) { + $user = get_userdata( $data['ID'] ); + if ( ! $user ) { + return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); + } + + if ( ! current_user_can( 'edit_user', $data['ID'] ) ) { + return new WP_Error( 'json_user_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => 403 ) ); + } + + $required = array( 'username', 'password', 'email' ); + foreach ( $required as $arg ) { + if ( empty( $data[ $arg ] ) ) { + return new WP_Error( 'json_missing_callback_param', sprintf( __( 'Missing parameter %s' ), $arg ), array( 'status' => 400 ) ); + } + } + + $update = true; + } + else { + $user = new WP_User(); + + if ( ! current_user_can( 'create_users' ) ) { + return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to create users.' ), array( 'status' => 403 ) ); + } + + $update = false; + } + + // Basic authentication details + if ( isset( $data['username'] ) ) { + $user->user_login = $data['username']; + } + if ( isset( $data['password'] ) ) { + $user->user_pass = $data['password']; + } + + // Names + if ( isset( $data['name'] ) ) { + $user->display_name = $data['name']; + } + if ( isset( $data['first_name'] ) ) { + $user->first_name = $data['first_name']; + } + if ( isset( $data['last_name'] ) ) { + $user->last_name = $data['last_name']; + } + if ( isset( $data['nickname'] ) ) { + $user->nickname = $data['nickname']; + } + if ( ! empty( $data['slug'] ) ) { + $user->user_nicename = $data['slug']; + } + + // URL + if ( ! empty( $data['URL'] ) ) { + $escaped = esc_url_raw( $user->user_url ); + if ( $escaped !== $user->user_url ) { + return new WP_Error( 'json_invalid_url', __( 'Invalid user URL.' ), array( 'status' => 400 ) ); + } + + $user->user_url = $data['URL']; + } + + // Description + if ( ! empty( $data['description'] ) ) { + $user->description = $data['description']; + } + + // Email + if ( ! empty( $data['email'] ) ) { + $user->user_email = $data['email']; + } + + // Pre-flight check + $user = apply_filters( 'json_pre_insert_user', $user, $data ); + if ( is_wp_error( $user ) ) { + return $user; + } + + $user_id = $update ? wp_update_user( $user ) : wp_insert_user( $user ); + if ( is_wp_error( $user_id ) ) { + return $user_id; + } + + $user->ID = $user_id; + + do_action( 'json_insert_user', $user, $data, $update ); + + return $user_id; + } + /** * Edit a user. * @@ -183,24 +276,14 @@ public function edit_user( $id, $data, $_headers = array() ) { return new WP_Error( 'json_user_invalid_id', __( 'User ID is invalid.' ), array( 'status' => 400 ) ); } - // Update attributes of the user from $data - $retval = $this->update_user( $user, $data ); - if ( is_wp_error( $retval ) ) { - return $retval; - } - - // Pre-update hook - $user = apply_filters( 'json_pre_update_user', $user, $id, $data, $_headers ); + $data['ID'] = $user->ID; - // Update the user in the database - // http://codex.wordpress.org/Function_Reference/wp_update_user - $retval = wp_update_user( $user ); + // Update attributes of the user from $data + $retval = $this->insert_user( $data ); if ( is_wp_error( $retval ) ) { return $retval; } - do_action( 'json_insert_user', $user, $data, true ); // $update is always true - return $this->get_user( $id ); } @@ -211,56 +294,15 @@ public function edit_user( $id, $data, $_headers = array() ) { * @return mixed */ public function new_user( $data ) { - # TODO: Use WP_User here, and refactor so we're sharing code with edit_user - # You can provide a WP_User to wp_insert_user. But the semantics of using WP_User to create a new - # user aren't defined well in the documentation: it doesn't even tell you how to create a WP_User without - # using an ID. So we won't try to do that. - # - # https://codex.wordpress.org/Function_Reference/wp_insert_user - provide complete user_data - # https://codex.wordpress.org/Function_Reference/wp_create_user - just takes username, password, email - # https://codex.wordpress.org/Class_Reference/WP_User - # http://tommcfarlin.com/create-a-user-in-wordpress/ - if ( ! current_user_can( 'create_users' ) ) { return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to create users.' ), array( 'status' => 403 ) ); } - if ( empty( $data['username'] ) ) { - return new WP_Error( 'json_user_username_missing', __( 'No username supplied.'), array( 'status' => 400 ) ); - } - if ( empty( $data['password'] ) ) { - return new WP_Error( 'json_user_password_missing', __( 'No password supplied.' ), array( 'status' => 400 ) ); - } - if ( empty( $data['email'] ) ) { - return new WP_Error( 'json_user_email_missing', __( 'No email supplied.' ), array( 'status' => 400 ) ); - } - - $userdata = array( - // These are the required fields for wp_insert_user - 'user_login' => $data['username'], // must be unique - 'user_pass' => $data['password'], // we're sending this in plain text - 'user_email' => $data['email'], // must be unique - ); - - if ( ! empty( $data['slug'] ) ) { - $userdata['user_nicename'] = $data['slug']; // TODO: This is made unique eg to Fred-3 - what to do? - } - if ( ! empty( $data['name'] ) ) { - $userdata['display_name'] = $data['name']; - } - if ( ! empty( $data['URL'] ) ) { - $userdata['user_url'] = $data['URL']; - } - - - // Pre-insert hook - // TODO: Or json_pre_insert_user? Or insert rather than create? "Insert" seems to mean create or edit in WP...? - $userdata = apply_filters( 'json_pre_create_user', $userdata, $data ); - if ( is_wp_error( $userdata ) ) { - return $userdata; + if ( ! empty( $data['ID'] ) ) { + return new WP_Error( 'json_user_exists', __( 'Cannot create existing user.' ), array( 'status' => 400 ) ); } - $user_id = wp_insert_user( $userdata ); + $user_id = $this->insert_user( $data ); // TODO: Send appropriate HTTP error codes along with the JSON rendering of the WP_Error we send back // TODO: I guess we can just add/overwrite the 'status' code in there ourselves... nested WP_Error? // These are the errors wp_insert_user() might return (from the wp_create_user documentation) @@ -316,45 +358,4 @@ public function delete_user( $id, $force = false ) { return array( 'message' => __( 'Deleted user' ) ); } } - - /** - * - * Update a WP_User instance from a User entity. - * - * @param WP_User $user - * @param $data - */ - protected function update_user( $user, $data ) { - - // Won't let them update these fields: ID, login, pass, registered (silently ignored) - // TODO: Raise an exception if they try to update those? Always ignore ID though. - - // Note that you can pass wp_update_user() an array of fields to - // update; we won't bother using it as they don't match the User entity - // and it's just one more level of indirection to maintain. - - // https://github.com/WP-API/WP-API/blob/master/docs/schema.md#user - // http://codex.wordpress.org/Class_Reference/WP_User - // http://wpsmith.net/2012/wp/an-introduction-to-wp_user-class/ - - // There are more fields in WP_User we might - - if ( ! empty( $data['name'] ) ) { - $user->display_name = $data[ 'name' ]; - } - if ( ! empty( $data['slug'] ) ) { - $user->user_nicename = $data[ 'slug' ]; - } - if ( ! empty( $data['URL'] ) ) { - $user->user_url = $data[ 'URL' ]; - } - // ignore avatar - read-only - // ignore username - can't change this - if ( ! empty( $data['email'] ) ) { - $user->user_email = $data['email']; - } - // ignore registered - probably no need - - } - } From 156dd9827dba38f7950af010424743b235d14fed Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 25 Apr 2014 12:11:55 +1000 Subject: [PATCH 055/515] Allow a user to view themselves --- lib/class-wp-json-users.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 305defb86d..ab7cdf0d59 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -101,7 +101,8 @@ public function get_users( $filter = array(), $context = 'view', $page = 1 ) { * @return response */ public function get_user( $id, $context = 'view' ) { - if ( ! current_user_can( 'list_users' ) ) { + $current_user_id = get_current_user_id(); + if ( $current_user_id !== $id && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'json_user_cannot_list', __( 'Sorry, you are not allowed to view this user.' ), array( 'status' => 403 ) ); } From 932c4076895a4b3f74c6a12fd515e0a2f1e2fbc5 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 25 Apr 2014 12:12:05 +1000 Subject: [PATCH 056/515] Workaround core bug with empty WP_User --- lib/class-wp-json-users.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index ab7cdf0d59..aca90634bd 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -179,6 +179,9 @@ protected function insert_user( $data ) { else { $user = new WP_User(); + // Workaround for https://core.trac.wordpress.org/ticket/28019 + $user->data = new stdClass; + if ( ! current_user_can( 'create_users' ) ) { return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to create users.' ), array( 'status' => 403 ) ); } From ac19e09b391307c159b9806bcc43a6bbf3afa07d Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 25 Apr 2014 12:12:21 +1000 Subject: [PATCH 057/515] Ensure we always have a Response --- lib/class-wp-json-users.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index aca90634bd..c9b648f609 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -321,6 +321,10 @@ public function new_user( $data ) { } $response = $this->get_user( $user_id ); + if ( ! $response instanceof WP_JSON_ResponseInterface ) { + $response = new WP_JSON_Response( $response ); + } + $response->set_status( 201 ); $response->header( 'Location', json_url( '/users/' . $user_id ) ); return $response; From 8e5a060484666c68fe854340fb42a42981a39f0b Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 25 Apr 2014 19:50:09 -0500 Subject: [PATCH 058/515] Delete the plugin option on deactivation and null shouldn't be a constant. --- plugin.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin.php b/plugin.php index 131d005fa9..155be957af 100644 --- a/plugin.php +++ b/plugin.php @@ -160,7 +160,7 @@ function json_api_activation( $network_wide ) { switch_to_blog( $mu_blog['blog_id'] ); json_api_register_rewrites(); - update_option( 'json_api_plugin_version', NULL ); + update_option( 'json_api_plugin_version', null ); } restore_current_blog(); @@ -168,7 +168,7 @@ function json_api_activation( $network_wide ) { } else { json_api_register_rewrites(); - update_option( 'json_api_plugin_version', NULL ); + update_option( 'json_api_plugin_version', null ); } } register_activation_hook( __FILE__, 'json_api_activation' ); @@ -184,14 +184,14 @@ function json_api_deactivation( $network_wide ) { foreach ( $mu_blogs as $mu_blog ) { switch_to_blog( $mu_blog['blog_id'] ); - update_option( 'json_api_plugin_version', NULL ); + delete_option( 'json_api_plugin_version' ); } restore_current_blog(); } else { - update_option( 'json_api_plugin_version', NULL ); + delete_option( 'json_api_plugin_version' ); } } register_deactivation_hook( __FILE__, 'json_api_deactivation' ); From 71107a42037129497000b6156d5e846f640aff22 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 25 Apr 2014 19:54:25 -0500 Subject: [PATCH 059/515] Null default value for 'json_api_plugin_version' option should be in lower case. --- plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.php b/plugin.php index 155be957af..ce642a4acb 100644 --- a/plugin.php +++ b/plugin.php @@ -52,7 +52,7 @@ function json_api_register_rewrites() { * Determine if the rewrite rules should be flushed. */ function json_api_maybe_flush_rewrites() { - $version = get_option( 'json_api_plugin_version', NULL ); + $version = get_option( 'json_api_plugin_version', null ); if ( empty( $version ) || $version !== JSON_API_VERSION ) { flush_rewrite_rules(); From cfa84e53b053172b8c9371149d344bcda267467a Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sat, 26 Apr 2014 11:11:06 -0500 Subject: [PATCH 060/515] Corrected method name of return value in WP_JSON_Posts::edit_post() method. Fixes #150 --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 19aad44f2d..394afc2221 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -314,7 +314,7 @@ function edit_post( $id, $data, $_headers = array() ) { return $retval; } - return $this->getPost( $id ); + return $this->get_post( $id ); } /** From ddcbd4db35bf595c185ef6200d0d484e7ef203f3 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 28 Apr 2014 21:26:31 +1000 Subject: [PATCH 061/515] Remove post show_in_json debugging line Accidentally committed this, apologies! --- plugin.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugin.php b/plugin.php index 63949a7568..9f6080b6db 100644 --- a/plugin.php +++ b/plugin.php @@ -234,10 +234,6 @@ function json_register_post_type( $post_type, $args ) { $type = &$wp_post_types[ $post_type ]; - if ( $post_type === 'post' ) { - $type->show_in_json = false; - } - // Exception for pages if ( $post_type === 'page' ) { $type->show_in_json = true; From 140ae7d9f7187e8377d93648cdd02170c5e3b563 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 11:34:18 +1000 Subject: [PATCH 062/515] Remove basic authentication handler Also removes the json_check_authentication filter. All authentication handlers should switch to the determine_current_user filter added in https://core.trac.wordpress.org/changeset/27484 Adds a json_authentication_errors filter to allow reporting a WP_Error back to the API on failed authentication. --- lib/class-wp-json-server.php | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index bbf0d9c553..7c98555986 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -93,26 +93,10 @@ class WP_JSON_Server implements WP_JSON_ResponseHandler { /** * Check the authentication headers if supplied * - * @return WP_Error|WP_User|null WP_User object indicates successful login, WP_Error indicates unsuccessful login and null indicates no authentication provided + * @return WP_Error|null WP_Error indicates unsuccessful login, null indicates successful or no authentication provided */ public function check_authentication() { - $user = apply_filters( 'json_check_authentication', null ); - if ( is_a( $user, 'WP_User' ) ) - return $user; - - if ( !isset( $_SERVER['PHP_AUTH_USER'] ) ) - return; - - $username = $_SERVER['PHP_AUTH_USER']; - $password = $_SERVER['PHP_AUTH_PW']; - - $user = wp_authenticate( $username, $password ); - - if ( is_wp_error( $user ) ) - return $user; - - wp_set_current_user( $user->ID ); - return $user; + return apply_filters( 'json_authentication_errors', null ); } /** From ab280a40871251bd922bfab71b08c99399f3c12d Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 11:48:03 +1000 Subject: [PATCH 063/515] Remove basic authentication test This test will intentionally fail now that basic auth has been removed. --- tests/test_json_server.php | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/tests/test_json_server.php b/tests/test_json_server.php index d67256ceea..702960062f 100644 --- a/tests/test_json_server.php +++ b/tests/test_json_server.php @@ -31,27 +31,6 @@ function setUp() { $wp_json_server = new $wp_json_server_class; } - /** - * The server should be able to authenticate users using basic auth. - */ - function test_valid_basic_auth() { - global $wp_json_server; - - $user_id = $this->factory->user->create( array( - 'user_login' => 'basic_auth', - 'user_pass' => 'basic_auth' - ) ); - - $_SERVER['PHP_AUTH_USER'] = 'basic_auth'; - $_SERVER['PHP_AUTH_PW'] = 'basic_auth'; - - $result = $wp_json_server->check_authentication(); - $this->assertTrue( $result instanceof WP_User ); - - unset( $_SERVER['PHP_AUTH_USER'] ); - unset( $_SERVER['PHP_AUTH_PW'] ); - } - /** * Errors should convert to arrays cleanly. */ From 881cb5e8146b380638928d2abfd0bca212fbf3fc Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 12:03:29 +1000 Subject: [PATCH 064/515] Add /users/me endpoint This returns the data for the current user, and also issues a 302 redirect to the current user's endpoint permalink (e.g. /users/42) --- lib/class-wp-json-server.php | 4 ---- lib/class-wp-json-users.php | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index e605e2b95a..fdd74f5f80 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -293,10 +293,6 @@ public function get_routes() { $endpoints = array( // Meta endpoints '/' => array( array( $this, 'get_index' ), self::READABLE ), - - // Users - // /users/me is an alias, and simply redirects to /users/ - '/users/me' => array( '__return_null', self::ALLMETHODS ), ); $endpoints = apply_filters( 'json_endpoints', $endpoints ); diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index c9b648f609..b92478dd30 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -49,7 +49,11 @@ public function register_routes( $routes ) { array( array( $this, 'get_user' ), WP_JSON_Server::READABLE ), array( array( $this, 'edit_user' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), array( array( $this, 'delete_user' ), WP_JSON_Server::DELETABLE ), - ) + ), + // /users/me is an alias, and simply redirects to /users/ + '/users/me' => array( + array( array( $this, 'get_current_user' ), WP_JSON_Server::READABLE ), + ), ); return array_merge( $routes, $user_routes ); } @@ -93,6 +97,35 @@ public function get_users( $filter = array(), $context = 'view', $page = 1 ) { return $struct; } + /** + * Retrieve the current user + * + * @param string $context + * @return mixed See + */ + public function get_current_user( $context = 'view' ) { + $current_user_id = get_current_user_id(); + if ( empty( $current_user_id ) ) { + return new WP_Error( 'json_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) ); + } + + $response = $this->get_user( $current_user_id, $context ); + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( ! ( $response instanceof WP_JSON_ResponseInterface ) ) { + $response = new WP_JSON_Response( $response ); + } + + $data = $response->get_data(); + + $response->header( 'Location', $data['meta']['links']['self'] ); + $response->set_status( 302 ); + + return $response; + } + /** * Retrieve a user. * From a4352b4ea7e6531b268b46bba57af5edd76ecb7e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 12:24:16 +1000 Subject: [PATCH 065/515] Allow reassigning posts when deleting a user --- lib/class-wp-json-users.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index b92478dd30..b62e4869ec 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -370,7 +370,7 @@ public function new_user( $data ) { * @param bool force * @return true on success */ - public function delete_user( $id, $force = false ) { + public function delete_user( $id, $force = false, $reassign = null ) { $id = absint( $id ); if ( empty( $id ) ) { @@ -388,9 +388,19 @@ public function delete_user( $id, $force = false ) { return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 400 ) ); } - // https://codex.wordpress.org/Function_Reference/wp_delete_user - // TODO: Allow posts to be reassigned (see the docs for wp_delete_user) - use a HTTP parameter? - $result = wp_delete_user( $id ); + if ( ! empty( $reassign ) ) { + $reassign = absint( $reassign ); + + // Check that reassign is valid + if ( empty( $reassign ) || $reassign === $id || ! get_userdata( $reassign ) ) { + return new WP_Error( 'json_user_invalid_reassign', __( 'Invalid user ID.' ), array( 'status' => 400 ) ); + } + } + else { + $reassign = null; + } + + $result = wp_delete_user( $id, $reassign ); if ( ! $result ) { return new WP_Error( 'json_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 500 ) ); From 4df9ed4d819739b9d1627a5975b64f596c0ba170 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 12:29:53 +1000 Subject: [PATCH 066/515] Clean up comments --- lib/class-wp-json-users.php | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index b62e4869ec..8cb4d26d8b 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -1,20 +1,5 @@ 403 ) ); } @@ -176,7 +160,7 @@ protected function prepare_user( $user, $context = 'view' ) { if ( $context === 'edit' ) { // The user's specific caps should only be needed if you're editing - // the user, as allcaps should handle most users + // the user, as allcaps should handle most uses $user_fields['extra_capabilities'] = $user->caps; } @@ -340,15 +324,6 @@ public function new_user( $data ) { } $user_id = $this->insert_user( $data ); - // TODO: Send appropriate HTTP error codes along with the JSON rendering of the WP_Error we send back - // TODO: I guess we can just add/overwrite the 'status' code in there ourselves... nested WP_Error? - // These are the errors wp_insert_user() might return (from the wp_create_user documentation) - // - empty_user_login, Cannot create a user with an empty login name. => BAD_REQUEST - // - existing_user_login, This username is already registered. => CONFLICT - // - existing_user_email, This email address is already registered. => CONFLICT - // http://stackoverflow.com/questions/942951/rest-api-error-return-good-practices - // http://stackoverflow.com/questions/3825990/http-response-code-for-post-when-resource-already-exists - // http://soabits.blogspot.com/2013/05/error-handling-considerations-and-best.html if ( is_wp_error( $user_id ) ) { return $user_id; } From 5e47b032b7038ac8710d3ce1f6cc9b309d4f094f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 12:38:00 +1000 Subject: [PATCH 067/515] Use user class to add post author data --- lib/class-wp-json-posts.php | 2 -- lib/class-wp-json-users.php | 18 ++++++++++++++++++ plugin.php | 1 + 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 76a74f212b..6a175c7a83 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -556,8 +556,6 @@ protected function prepare_post( $post, $context = 'view' ) { if ( empty( $post_fields['format'] ) ) $post_fields['format'] = 'standard'; - $post_fields['author'] = $this->prepare_author( $post['post_author'] ); - if ( 'view' === $context && 0 !== $post['post_parent'] ) { // Avoid nesting too deeply // This gives post + post-extended + meta for the main post, diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 8cb4d26d8b..6af99ab334 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -173,6 +173,24 @@ protected function prepare_user( $user, $context = 'view' ) { return apply_filters( 'json_prepare_user', $user_fields, $user, $context ); } + /** + * Add author data to post data + * + * @param array $data Post data + * @param array $post Internal post data + * @param string $context Post context + * @return array Filtered data + */ + public function add_post_author_data( $data, $post, $context ) { + $author = get_userdata( $post['post_author'] ); + + if ( ! empty( $author ) ) { + $data['author'] = $this->prepare_user( $author, 'embed' ); + } + + return $data; + } + protected function insert_user( $data ) { if ( ! empty( $data['ID'] ) ) { $user = get_userdata( $data['ID'] ); diff --git a/plugin.php b/plugin.php index 1797076965..686a64da67 100644 --- a/plugin.php +++ b/plugin.php @@ -78,6 +78,7 @@ function json_api_default_filters($server) { // Users $wp_json_users = new WP_JSON_Users($server); add_filter( 'json_endpoints', array( $wp_json_users, 'register_routes' ), 0 ); + add_filter( 'json_prepare_post', array( $wp_json_users, 'add_post_author_data' ), 10, 3 ); // Pages $wp_json_pages = new WP_JSON_Pages($server); From 38fd056ffcacae9a8ab8fb3c42279524015fd711 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 12:42:43 +1000 Subject: [PATCH 068/515] Add json_prepare_comment filter --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 6a175c7a83..fae56f5ce0 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1024,7 +1024,7 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen if ( in_array( 'meta', $requested_fields ) ) $data['meta'] = $meta; - return $data; + return apply_filters( 'json_prepare_comment', $data, $comment, $context ); } /** From 484dbdf48a7b29dabc40e5d02d94311141549897 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 12:43:40 +1000 Subject: [PATCH 069/515] Use user class to add comment author data --- lib/class-wp-json-posts.php | 2 +- lib/class-wp-json-users.php | 20 ++++++++++++++++++++ plugin.php | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index fae56f5ce0..68ffc899e2 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -984,7 +984,7 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen // Author if ( (int) $comment->user_id !== 0 ) { - $fields['author'] = $this->prepare_author( (int) $comment->user_id ); + $fields['author'] = (int) $comment->user_id; } else { $fields['author'] = array( diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 6af99ab334..742bc3329c 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -191,6 +191,26 @@ public function add_post_author_data( $data, $post, $context ) { return $data; } + /** + * Add author data to comment data + * + * @param array $data Comment data + * @param array $comment Internal comment data + * @param string $context Data context + * @return array Filtered data + */ + public function add_comment_author_data( $data, $comment, $context ) { + if ( (int) $comment->user_id !== 0 ) { + $author = get_userdata( $comment->user_id ); + + if ( ! empty( $author ) ) { + $data['author'] = $this->prepare_user( $author, 'embed' ); + } + } + + return $data; + } + protected function insert_user( $data ) { if ( ! empty( $data['ID'] ) ) { $user = get_userdata( $data['ID'] ); diff --git a/plugin.php b/plugin.php index 686a64da67..4adea53a3d 100644 --- a/plugin.php +++ b/plugin.php @@ -79,6 +79,7 @@ function json_api_default_filters($server) { $wp_json_users = new WP_JSON_Users($server); add_filter( 'json_endpoints', array( $wp_json_users, 'register_routes' ), 0 ); add_filter( 'json_prepare_post', array( $wp_json_users, 'add_post_author_data' ), 10, 3 ); + add_filter( 'json_prepare_comment', array( $wp_json_users, 'add_comment_author_data' ), 10, 3 ); // Pages $wp_json_pages = new WP_JSON_Pages($server); From 5e01598017d19184f19d780d129b9450a974da63 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 12:44:09 +1000 Subject: [PATCH 070/515] Realign user filter registration calls --- plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.php b/plugin.php index 4adea53a3d..0263994b29 100644 --- a/plugin.php +++ b/plugin.php @@ -77,8 +77,8 @@ function json_api_default_filters($server) { // Users $wp_json_users = new WP_JSON_Users($server); - add_filter( 'json_endpoints', array( $wp_json_users, 'register_routes' ), 0 ); - add_filter( 'json_prepare_post', array( $wp_json_users, 'add_post_author_data' ), 10, 3 ); + add_filter( 'json_endpoints', array( $wp_json_users, 'register_routes' ), 0 ); + add_filter( 'json_prepare_post', array( $wp_json_users, 'add_post_author_data' ), 10, 3 ); add_filter( 'json_prepare_comment', array( $wp_json_users, 'add_comment_author_data' ), 10, 3 ); // Pages From 31666af2e0a8eb8c7b4590703bed45d2a51dfaae Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 12:45:45 +1000 Subject: [PATCH 071/515] Hide roles/caps on 'embed' context --- lib/class-wp-json-users.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 742bc3329c..156571d879 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -137,7 +137,7 @@ public function get_user( $id, $context = 'view' ) { * Prepare a User entity from a WP_User instance. * * @param WP_User $user - * @param string $context + * @param string $context One of 'view', 'edit', 'embed' * @return array */ protected function prepare_user( $user, $context = 'view' ) { @@ -154,10 +154,13 @@ protected function prepare_user( $user, $context = 'view' ) { 'description' => $user->description, 'email' => $user->user_email, 'registered' => $user->user_registered, - 'roles' => $user->roles, - 'capabilities' => $user->allcaps, ); + if ( $context === 'view' || $context === 'edit' ) { + $user_fields['roles'] = $user->roles; + $user_fields['capabilities'] = $user->allcaps; + } + if ( $context === 'edit' ) { // The user's specific caps should only be needed if you're editing // the user, as allcaps should handle most uses From e189a1ee08080db2e8af59ac2e67a20a8471b0c0 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 12:48:18 +1000 Subject: [PATCH 072/515] Ensure registration date/time is in RFC3339 form --- lib/class-wp-json-users.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 156571d879..4110a05bbc 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -153,9 +153,10 @@ protected function prepare_user( $user, $context = 'view' ) { 'avatar' => $this->server->get_avatar_url( $user->user_email ), 'description' => $user->description, 'email' => $user->user_email, - 'registered' => $user->user_registered, ); + $user_fields['registered'] = date( 'c', strtotime( $user->user_registered ) ); + if ( $context === 'view' || $context === 'edit' ) { $user_fields['roles'] = $user->roles; $user_fields['capabilities'] = $user->allcaps; From 5c327f4d202852738e7076e1d46e17b5f3c1d89a Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 12:49:27 +1000 Subject: [PATCH 073/515] Add user endpoint tests --- tests/test_json_users.php | 143 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 tests/test_json_users.php diff --git a/tests/test_json_users.php b/tests/test_json_users.php new file mode 100644 index 0000000000..1877978cf6 --- /dev/null +++ b/tests/test_json_users.php @@ -0,0 +1,143 @@ +user = $this->factory->user->create(); + wp_set_current_user( $this->user ); + $this->user_obj = wp_get_current_user(); + + $this->fake_server = $this->getMock('WP_JSON_Server'); + $this->endpoint = new WP_JSON_Users( $this->fake_server ); + } + + public function test_get_current_user() { + $response = $this->endpoint->get_current_user(); + $this->assertNotInstanceOf( 'WP_Error', $response ); + + if ( ! $response instanceof WP_JSON_ResponseInterface ) { + $response = new WP_JSON_Response( $response ); + } + + // Check that we succeeded + $this->assertEquals( 302, $response->get_status() ); + + $headers = $response->get_headers(); + $response_data = $response->get_data(); + $this->assertArrayHasKey( 'Location', $headers ); + $this->assertEquals( $response_data['meta']['links']['self'], $headers['Location'] ); + + $this->check_get_user_response( $response, $this->user_obj ); + + } + + public function test_get_user() { + $response = $this->endpoint->get_user( $this->user ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + + if ( ! $response instanceof WP_JSON_ResponseInterface ) { + $response = new WP_JSON_Response( $response ); + } + + // Check that we succeeded + $this->assertEquals( 200, $response->get_status() ); + + $this->check_get_user_response( $response, $this->user_obj ); + } + + protected function check_get_user_response( $response, $user_obj ) { + $response_data = $response->get_data(); + + // Check basic data + $this->assertEquals( $user_obj->ID, $response_data['ID'] ); + $this->assertEquals( $user_obj->user_login, $response_data['username'] ); + $this->assertEquals( $user_obj->user_email, $response_data['email'] ); + + // Check that we didn't get extra data + $this->assertArrayNotHasKey( 'extra_capabilities', $response_data ); + } + + public function test_create_user() { + $this->user_obj->set_role( 'administrator' ); + $data = array( + 'username' => 'test_user', + 'password' => 'test_password', + 'email' => 'test@example.com', + ); + $response = $this->endpoint->new_user( $data ); + + // Check that we succeeded + $this->assertNotInstanceOf( 'WP_Error', $response ); + $this->assertEquals( 201, $response->get_status() ); + + $response_data = $response->get_data(); + + // Check that the data is intact + $new_user = get_userdata( $response_data['ID'] ); + + $this->assertEquals( $data['username'], $response_data['username'] ); + $this->assertEquals( $data['username'], $new_user->user_login ); + + $this->assertEquals( $data['email'], $response_data['email'] ); + $this->assertEquals( $data['email'], $new_user->user_email ); + + $this->assertTrue( wp_check_password( $data['password'], $new_user->user_pass ), 'Password check failed' ); + } + + public function test_delete_user() { + $this->user_obj->set_role( 'administrator' ); + + // Test with a new user, rather than ourselves, to avoid any + // complications with doing so. We should check this separately though. + $test_user = $this->factory->user->create(); + $response = $this->endpoint->delete_user( $test_user ); + + $this->assertNotInstanceOf( 'WP_Error', $response ); + + if ( ! $response instanceof WP_JSON_ResponseInterface ) { + $response = new WP_JSON_Response( $response ); + } + + // Check that we succeeded + $this->assertEquals( 200, $response->get_status() ); + } + + public function test_delete_user_reassign() { + $this->user_obj->set_role( 'administrator' ); + + // Test with a new user, rather than ourselves, to avoid any + // complications with doing so. We should check this separately though. + $test_user = $this->factory->user->create(); + $test_new_author = $this->factory->user->create(); + $test_post = $this->factory->post->create(array( + 'post_author' => $test_user, + )); + + // Sanity check to ensure the factory created the post correctly + $post = get_post( $test_post ); + $this->assertEquals( $test_user, $post->post_author ); + + // Delete our test user, and reassign to the new author + $response = $this->endpoint->delete_user( $test_user, false, $test_new_author ); + + $this->assertNotInstanceOf( 'WP_Error', $response ); + + if ( ! $response instanceof WP_JSON_ResponseInterface ) { + $response = new WP_JSON_Response( $response ); + } + + // Check that we succeeded + $this->assertEquals( 200, $response->get_status() ); + + // Check that the post has been updated correctly + $post = get_post( $test_post ); + $this->assertEquals( $test_new_author, $post->post_author ); + } +} From 1c72d1c7b1f94579232ec1a200d31d0a67953ee3 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 23:26:03 +1000 Subject: [PATCH 074/515] Add json_ensure_response This ensures that a value is either a WP_JSON_ResponseInterface object, or a WP_Error, by wrapping any other value in a WP_JSON_Response instance. --- lib/class-wp-json-posts.php | 2 +- plugin.php | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 76a74f212b..ffa30f6e04 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -218,7 +218,7 @@ function new_post( $data ) { return $result; } - $response = $this->get_post( $result ); + $response = json_ensure_response( $this->get_post( $result ) ); $response->set_status( 201 ); $response->header( 'Location', json_url( '/posts/' . $result ) ); return $response; diff --git a/plugin.php b/plugin.php index a225c82a27..0fec1bba0a 100644 --- a/plugin.php +++ b/plugin.php @@ -286,3 +286,27 @@ function get_json_url( $blog_id = null, $path = '', $scheme = 'json' ) { function json_url( $path = '', $scheme = 'json' ) { return get_json_url( null, $path, $scheme ); } + +/** + * Ensure a JSON response is a response object + * + * This ensures that the response is consistent, and implements + * {@see WP_JSON_ResponseInterface}, allowing usage of + * `set_status`/`header`/etc without needing to double-check the object. Will + * also allow {@see WP_Error} to indicate error responses, so users should + * immediately check for this value. + * + * @param WP_Error|WP_JSON_ResponseInterface|mixed $response Response to check + * @return WP_Error|WP_JSON_ResponseInterface + */ +function json_ensure_response( $response ) { + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( $response instanceof WP_JSON_ResponseInterface ) { + return $response; + } + + return new WP_JSON_Response( $response ); +} From be6bcc08b01295ac9ae47e440dc92f20ae9b45fd Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 29 Apr 2014 23:48:30 +1000 Subject: [PATCH 075/515] Ensure we handle raw data passed to the API --- lib/class-wp-json-server.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index bbf0d9c553..db105029f8 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -390,6 +390,9 @@ public function dispatch() { } elseif ( $supported & self::ACCEPT_RAW ) { $data = $this->get_raw_data(); + if ( ! empty( $data ) ) { + $args = array_merge( $args, array( 'data' => $data ) ); + } } $args['_method'] = $method; From bd9fd6afed87a6fda1c9c38cbda1f571633b811b Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 3 May 2014 16:10:38 +1000 Subject: [PATCH 076/515] Allow multiple post types to get_posts --- lib/class-wp-json-posts.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index e9975e691f..ca39bdd399 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -80,11 +80,15 @@ public function register_routes( $routes ) { public function get_posts( $filter = array(), $context = 'view', $type = 'post', $page = 1 ) { $query = array(); - $post_type = get_post_type_object( $type ); - if ( ! ( (bool) $post_type ) || ! $post_type->show_in_json ) - return new WP_Error( 'json_invalid_post_type', __( 'The post type specified is not valid' ), array( 'status' => 403 ) ); + // Validate post types and permissions + $query['post_type'] = array(); + foreach ( (array) $type as $post_type ) { + $post_type = get_post_type_object( $post_type ); + if ( ! ( (bool) $post_type ) || ! $post_type->show_in_json ) + return new WP_Error( 'json_invalid_post_type', __( 'The post type specified is not valid' ), array( 'status' => 403 ) ); - $query['post_type'] = $post_type->name; + $query['post_type'][] = $post_type->name; + } global $wp; // Allow the same as normal WP From ec04d6c349a25cfe7419348060bbbd189ffb2ae5 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 3 May 2014 16:11:48 +1000 Subject: [PATCH 077/515] Provide invalid type name in error message --- lib/class-wp-json-posts.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index ca39bdd399..3e335957f7 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -82,10 +82,10 @@ public function get_posts( $filter = array(), $context = 'view', $type = 'post', // Validate post types and permissions $query['post_type'] = array(); - foreach ( (array) $type as $post_type ) { - $post_type = get_post_type_object( $post_type ); + foreach ( (array) $type as $type_name ) { + $post_type = get_post_type_object( $type_name ); if ( ! ( (bool) $post_type ) || ! $post_type->show_in_json ) - return new WP_Error( 'json_invalid_post_type', __( 'The post type specified is not valid' ), array( 'status' => 403 ) ); + return new WP_Error( 'json_invalid_post_type', sprintf( __( 'The post type "%s" is not valid' ), $type_name ), array( 'status' => 403 ) ); $query['post_type'][] = $post_type->name; } From 74218af2838bf4500bce71ea528f518272b7a00d Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 3 May 2014 16:15:49 +1000 Subject: [PATCH 078/515] Update get_posts phpDoc to match types change --- lib/class-wp-json-posts.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 3e335957f7..7112289186 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -73,9 +73,11 @@ public function register_routes( $routes ) { * @see WP_JSON_Posts::get_post() for more on $fields * @see get_posts() for more on $filter values * - * @param array $filter optional - * @param array $fields optional - * @return array contains a collection of Post entities. + * @param array $filter Parameters to pass through to `WP_Query` + * @param string $context + * @param string|array $type Post type slug, or array of slugs + * @param int $page Page number (1-indexed) + * @return stdClass[] Collection of Post entities */ public function get_posts( $filter = array(), $context = 'view', $type = 'post', $page = 1 ) { $query = array(); From ef79d939a94d1586683b2aecb4513aa47b02b2ed Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Sat, 3 May 2014 12:14:26 -0400 Subject: [PATCH 079/515] Return WP_Error in get_comment for invalid comments - resolves #166 --- lib/class-wp-json-posts.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index e9975e691f..69759dfddb 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -393,6 +393,10 @@ public function get_comments( $id ) { */ public function get_comment( $comment ) { $comment = get_comment( $comment ); + if ( empty( $comment ) ) { + return new WP_Error( 'json_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); + } + $data = $this->prepare_comment( $comment ); return $data; } From aabc7ff8ca8a5780a9a5800133da727239fa1ca8 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sat, 3 May 2014 11:15:37 -0500 Subject: [PATCH 080/515] Removed unused prepare_author method from WP_JSON_Posts class. Fixes #165. --- lib/class-wp-json-posts.php | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index e9975e691f..e3a76675d2 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -647,35 +647,6 @@ protected function prepare_meta( $post_id ) { return apply_filters( 'json_prepare_meta', $custom_fields, $post_id ); } - protected function prepare_author( $author ) { - $user = get_user_by( 'id', $author ); - - if (! $author || ! is_object( $user ) ) { - return null; - } - - - $author = array( - 'ID' => $user->ID, - 'name' => $user->display_name, - 'slug' => $user->user_nicename, - 'URL' => $user->user_url, - 'avatar' => $this->server->get_avatar_url( $user->user_email ), - 'meta' => array( - 'links' => array( - 'self' => json_url( '/users/' . $user->ID ), - 'archives' => json_url( '/users/' . $user->ID . '/posts' ), - ), - ), - ); - - if ( current_user_can( 'edit_user', $user->ID ) ) { - $author['first_name'] = $user->first_name; - $author['last_name'] = $user->last_name; - } - return $author; - } - /** * Helper method for wp_newPost and wp_editPost, containing shared logic. * From 9be17a8b24a62a6646fc7af64f513d701cee3a69 Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Mon, 28 Apr 2014 10:26:38 -0400 Subject: [PATCH 081/515] Post meta handler; Allow for updating, adding, and deleting of post meta. Filter for passing sanitization callbacks; filter for requiring santizition callbacks. This method assumes values are passed as they are intended to be stored (array or single). In response to #68 Remove sanitization callback functionality - see #68 --- lib/class-wp-json-posts.php | 55 +++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index acf545b7fe..002a87663e 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -629,12 +629,60 @@ protected function prepare_meta( $post_id ) { foreach ( $custom_fields as $meta_key => $meta_value ) { // Don't expose protected fields. if ( is_protected_meta( $meta_key ) ) - unset( $custom_fields[$meta_key] ); + unset( $custom_fields[$meta_key] ); } return apply_filters( 'json_prepare_meta', $custom_fields ); } + /** + * Update/add post meta for a post. This method relies on the user to provide sanitization callbacks. + * If none are provided, post meta will go unsanitized unless the require sanitization filter returns + * true. Post meta is expected to be sent like so: + * { + * post_meta : [ + * { + * key : 'meta_key', + * value : 'meta_value', (OPTIONAL) + * action : (update|add|delete) (OPTIONAL, default=update) + * } + * ] + * } + * + * @uses update_post_meta, apply_filters, add_post_meta, wp_parse_args, delete_post_meta + * @param array $data + * @param int $post_ID + * @return bool|WP_Error + */ + protected function insert_post_meta( $data, $post_ID ) { + if ( empty( $post_ID ) || empty( $data['post_meta'] ) || ! is_array( $data['post_meta'] ) ) { + return false; + } + + $meta_array_defaults = array( + 'action' => 'update', + 'value' => null, + ); + + foreach ( $data['post_meta'] as $meta_array ) { + + $meta_array = wp_parse_args( $meta_array, $meta_array_defaults ); + + if ( 'update' == $meta_array['action'] ) { + update_post_meta( $post_ID, $meta_array['key'], $meta_array['value'] ); + } elseif ( 'add' == $meta_array['action'] ) { + add_post_meta( $post_ID, $meta_array['key'], $meta_array['value'] ); + } elseif ( 'delete' == $meta_array['action'] ) { + delete_post_meta( $post_ID, $meta_array['key'] ); + } else { + // Return a WP_Error here? + continue; + } + } + + return true; + } + protected function prepare_author( $author ) { $user = get_user_by( 'id', $author ); @@ -747,7 +795,7 @@ protected function insert_post( $data ) { default: if ( ! get_post_status_object( $post['post_status'] ) ) $post['post_status'] = 'draft'; - break; + break; } } @@ -863,6 +911,9 @@ protected function insert_post( $data ) { return $post_ID; } + // Post meta + $this->insert_post_meta( $data, $post_ID ); + // Sticky if ( isset( $post['sticky'] ) ) { if ( $post['sticky'] ) From e4438c959bc7cd056f98f05f1beab602e22ca021 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sat, 3 May 2014 20:31:39 -0500 Subject: [PATCH 082/515] Added methods to verify post meta should be updated or added. --- lib/class-wp-json-posts.php | 88 ++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 431940c32b..467a797dc0 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -642,31 +642,29 @@ protected function prepare_meta( $post_id ) { // Don't expose protected fields. if ( is_protected_meta( $meta_key ) ) unset( $custom_fields[$meta_key] ); + } return apply_filters( 'json_prepare_meta', $custom_fields, $post_id ); } /** - * Update/add post meta for a post. This method relies on the user to provide sanitization callbacks. - * If none are provided, post meta will go unsanitized unless the require sanitization filter returns - * true. Post meta is expected to be sent like so: + * Update/add/delete post meta for a post. Post meta is expected to be sent like so: * { * post_meta : [ * { - * key : 'meta_key', - * value : 'meta_value', (OPTIONAL) - * action : (update|add|delete) (OPTIONAL, default=update) + * "key" : "meta_key", + * "value" : "meta_value", (OPTIONAL) + * "action" : "meta_action" (OPTIONAL, add|update|delete, default=update) * } * ] * } * - * @uses update_post_meta, apply_filters, add_post_meta, wp_parse_args, delete_post_meta * @param array $data - * @param int $post_ID + * @param int $post_id * @return bool|WP_Error */ - protected function insert_post_meta( $data, $post_ID ) { - if ( empty( $post_ID ) || empty( $data['post_meta'] ) || ! is_array( $data['post_meta'] ) ) { + protected function handle_post_meta_action( $post_id, $data ) { + if ( empty( $post_id ) || empty( $data ) || ! is_array( $data ) ) { return false; } @@ -675,25 +673,79 @@ protected function insert_post_meta( $data, $post_ID ) { 'value' => null, ); - foreach ( $data['post_meta'] as $meta_array ) { + foreach ( $data as $meta_array ) { + if ( empty( $meta_array['key'] ) ) { + return new WP_Error( 'json_post_invalid_action', __( 'Invalid meta action. Missing meta key.' ), array( 'status' => 400 ) ); + } $meta_array = wp_parse_args( $meta_array, $meta_array_defaults ); if ( 'update' == $meta_array['action'] ) { - update_post_meta( $post_ID, $meta_array['key'], $meta_array['value'] ); + $this->maybe_update_post_meta( $post_id, $meta_array['key'], $meta_array['value'] ); } elseif ( 'add' == $meta_array['action'] ) { - add_post_meta( $post_ID, $meta_array['key'], $meta_array['value'] ); + $this->maybe_add_post_meta( $post_id, $meta_array['key'], $meta_array['value'] ); } elseif ( 'delete' == $meta_array['action'] ) { - delete_post_meta( $post_ID, $meta_array['key'] ); + delete_post_meta( $post_id, $meta_array['key'] ); } else { - // Return a WP_Error here? - continue; + return new WP_Error( 'json_post_invalid_action', __( 'Invalid meta action.' ), array( 'status' => 400 ) ); + // continue; } } return true; } + /** + * Determines if meta key can be updated based on meta data type. + * If meta data is allowed by API, the values for the provided meta key is updated. + * + * @uses update_post_meta() + * + * @param int $post_id + * @param string $meta_key + * @param array $data + * @return bool|WP_Error + */ + protected function maybe_update_post_meta( $post_id, $meta_key, $data ) { + $old_data = get_post_meta( $post_id, $meta_key ); + + if ( is_object( $old_data ) || is_serialized( $old_data ) ) { + // for now let's not allow updating of arrays, objects or serialized values. + return new WP_Error( 'json_post_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); + } + if ( is_object( $data ) || is_serialized( $data ) ) { + // for now let's not allow updating of arrays, objects or serialized values. + return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); + } + + return update_post_meta( $post_id, $meta_key, $data ); + } + + /** + * Determines if meta key can be added based on meta data type. + * If meta data is allowed by API, the values for the provided meta key is added. + * + * @uses add_post_meta() + * + * @param int $post_id + * @param string $meta_key + * @param array $data + * @return bool|WP_Error + */ + protected function maybe_add_post_meta( $post_id, $meta_key, $data ) { + if ( is_object( $data ) || is_serialized( $data ) ) { + // for now let's not allow updating of arrays, objects or serialized values. + return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); + } + + $old_data = get_post_meta( $post_id, $meta_key ); + if ( ! empty( $old_data ) ) { + return new WP_Error( 'json_post_invalid_action', __( 'Invalid action provided for meta data.' ), array( 'status' => 400 ) ); + } + + return add_post_meta( $post_id, $meta_key, $data ); + } + protected function prepare_author( $author ) { $user = get_user_by( 'id', $author ); @@ -928,7 +980,9 @@ protected function insert_post( $data ) { } // Post meta - $this->insert_post_meta( $data, $post_ID ); + if ( ! empty( $data['post_meta'] ) ) { + $this->handle_post_meta_action( $post_ID, $data['post_meta'] ); + } // Sticky if ( isset( $post['sticky'] ) ) { From 78b2fbd12ec6bb2f46910ae7efc3ebe409c8de71 Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Sat, 3 May 2014 22:56:03 -0400 Subject: [PATCH 083/515] Add endpoint for post revisions; allow post revisions in JSON even though they aren't publically queryable [See #164] --- lib/class-wp-json-posts.php | 34 +++++++++++++++++++++++++++++++++- plugin.php | 5 +++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index e9975e691f..a76951cf51 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -36,7 +36,7 @@ public function register_routes( $routes ) { array( array( $this, 'edit_post' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), array( array( $this, 'delete_post' ), WP_JSON_Server::DELETABLE ), ), - '/posts/(?P\d+)/revisions' => array( '__return_null', WP_JSON_Server::READABLE ), + '/posts/(?P\d+)/revisions' => array( array( $this, 'get_revisions' ), WP_JSON_Server::READABLE ), // Comments '/posts/(?P\d+)/comments' => array( @@ -57,6 +57,38 @@ public function register_routes( $routes ) { return array_merge( $routes, $post_routes ); } + /** + * Get revisions for a specific post. + * + * @param int $id Post ID + * @uses wp_get_post_revisions + * @return WP_JSON_Response + */ + public function get_revisions( $id ) { + $id = (int) $id; + + // Todo: Should everyone be able to read post revisions? + + if ( empty( $id ) ) + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + + // Todo: Query args filter for wp_get_post_revisions + $revisions = wp_get_post_revisions( $id ); + + $response = new WP_JSON_Response(); + $struct = array(); + + foreach ( $revisions as $revision ) { + $post = get_object_vars( $revision ); + + $struct[] = $this->prepare_post( $post ); + } + + $response->set_data( $struct ); + + return $response; + } + /** * Retrieve posts. * diff --git a/plugin.php b/plugin.php index 2879de3da3..bbf01cd232 100644 --- a/plugin.php +++ b/plugin.php @@ -271,6 +271,11 @@ function json_register_post_type( $post_type, $args ) { $type->show_in_json = true; } + // Exception for revisions + if ( $post_type === 'revision' ) { + $type->show_in_json = true; + } + if ( ! isset( $type->show_in_json ) ) { $type->show_in_json = $type->publicly_queryable; } From 5dc7482047426b7eceac2ab705283dcb220a6492 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 4 May 2014 12:17:54 -0500 Subject: [PATCH 084/515] Initial pass at user endpoint documentation. --- docs/routes/users.md | 132 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 docs/routes/users.md diff --git a/docs/routes/users.md b/docs/routes/users.md new file mode 100644 index 0000000000..077ae9a93c --- /dev/null +++ b/docs/routes/users.md @@ -0,0 +1,132 @@ +Users +===== + +Retrieve Users +-------------- +The Users endpoint returns a User Collection containing a subset of the site's +users. + + GET /users + +### Input +#### `filter` +The `filter` parameter controls the query parameters. It is essentially a subset +of the parameters available to [`WP_User_Query`](http://codex.wordpress.org/Class_Reference/WP_User_Query). + +The parameter should be an array of the following key/value pairs: + +* `number` - Number of users to retrieve, use `-1` for all users. Default + is set by the site. (integer) +* `offset` - Number of posts to skip. Default is 0. (integer) +* `orderby` - Parameter to search by, as per [WP User_Query](https://codex.wordpress.org/Class_Reference/WP_User_Query#Order_.26_Orderby_Parameters). + Default is "user_login". (string) +* `order` - Order to sort by. Default is "ASC". (string, "ASC" or "DESC") +* `s` - Keyword to search for. (string) + +### Response +The response is a User Collection document containing the requested Users if +available. + + +Retrieve Current User +------------- + + GET /users/me + +### Input +#### `fields` +... + +### Response +The response is the User entity containing the current User if available. + + +Create a User +------------- + + POST /users + +### Input +The supplied data should be a User object. This data can be submitted via a +regular HTTP multipart body, with User values set as values to the `data` +parameter, or through a direct JSON body. + +That is, the following are equivalent: + +Content-Type: application/x-www-form-urlencoded + + data[username]=newuser&data[name]=New%20User&data[password]=secret + + +Content-Type: application/json + + {"username":"newuser","name":"New User","password":"secret"} + +### Response +On a successful creation, a 201 Created status is given, indicating that the +user has been created. The user is available canonically from the URL specified +in the Location header. + +The new User entity is also returned in the body for convienience. + + +Retrieve a User +--------------- + + GET /users/ + +### Input +#### `fields` +... + +### Response +The response is a User entity containing the requested User if available. + + +Edit a User +----------- + + PUT /users/ + +For compatibility reasons, this endpoint also accepts the POST and PATCH +methods. Both of these methods have the same behaviour as using PUT. It is +recommended to use PUT if available to fit with REST convention. + +### Input +The supplied data should be a User object. This data can be submitted via a +regular HTTP multipart body, with User values set as values to the `data` +parameter, or through a direct JSON body. See the Create User endpoint for an +example. + +### Response +On a successful update, a 200 OK status is given, indicating the user has been +updated. The updated User entity is returned in the body. + + +Delete a User +------------- + + DELETE /users/ + +### Input +#### `force` +The `force` parameter controls whether the user is permanently deleted or not. +By default, this is set to false, indicating that the user will be sent to an +intermediate storage (such as the trash) allowing it to be restored later. If +set to true, the user will not be able to be restored. + +Default is false. (boolean) + +#### `reassign` +The `reassign` parameter controls whether the deleted user's content is reassigned to a new User or not. By default, this is set to null, indicating that the deleted user's content will not be reassigned. +Default is null. (integer) + + +### Response +On successful deletion, a 202 Accepted status code will be returned, indicating +that the user has been moved to the trash for permanent deletion at a +later date. + +If force was set to true, a 200 OK status code will be returned instead, +indicating that the user has been permanently deleted. + From 93b831e8d31fff0d37a440d65f3d623ab657a4e9 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 12:13:50 +1000 Subject: [PATCH 085/515] Move current user docs to after get single user --- docs/routes/users.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/routes/users.md b/docs/routes/users.md index 077ae9a93c..5247f9169d 100644 --- a/docs/routes/users.md +++ b/docs/routes/users.md @@ -28,19 +28,6 @@ The response is a User Collection document containing the requested Users if available. -Retrieve Current User -------------- - - GET /users/me - -### Input -#### `fields` -... - -### Response -The response is the User entity containing the current User if available. - - Create a User ------------- @@ -83,6 +70,19 @@ Retrieve a User The response is a User entity containing the requested User if available. +Retrieve Current User +------------- + + GET /users/me + +### Input +#### `fields` +... + +### Response +The response is the User entity containing the current User if available. + + Edit a User ----------- From 927d74aa41992c9b798f420b901e68f55b6af356 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 12:21:11 +1000 Subject: [PATCH 086/515] Add more verbose documentation for /users/me --- docs/routes/users.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/routes/users.md b/docs/routes/users.md index 5247f9169d..73f10d7660 100644 --- a/docs/routes/users.md +++ b/docs/routes/users.md @@ -75,12 +75,21 @@ Retrieve Current User GET /users/me +This endpoint offers a permalink to get the current user, without needing to +know the user's ID. + ### Input #### `fields` ... ### Response -The response is the User entity containing the current User if available. +If the client is currently logged in, a 302 Found status is given. The User is +available canonically from the URL specified in the Location header. + +The User entity containing the current User is also returned in the body for +convenience. + +If the client is not logged in, a 401 Unauthorized status is given. Edit a User From 84850665b204ad8419f9d0e44592805ebefeecc1 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 12:27:41 +1000 Subject: [PATCH 087/515] Document available user data contexts --- docs/routes/users.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/routes/users.md b/docs/routes/users.md index 73f10d7660..0c4f4ed15e 100644 --- a/docs/routes/users.md +++ b/docs/routes/users.md @@ -63,11 +63,21 @@ Retrieve a User GET /users/ ### Input -#### `fields` -... +#### `context` +The `context` parameter controls the format of the data to return. The following +contexts are available: + +* `view`: The default context. Gives the normal User entity. +* `edit`: Context used for extra fields relevant to updating a user. Includes + the `extra_capabilities` field; this field contains the capabilities assigned + to the user themselves, rather than those inherited from their roles. +* `embed`: Context used when embedding the response inside another (e.g. post + author). This is intended as a minimal subset of the user data to reduce + response size. Excludes `roles` and `capabilities`. ### Response -The response is a User entity containing the requested User if available. +The response is a User entity containing the requested User if available. The +fields available on the User depend on the `context` parameter. Retrieve Current User @@ -79,15 +89,16 @@ This endpoint offers a permalink to get the current user, without needing to know the user's ID. ### Input -#### `fields` -... +#### `context` +The `context` parameter controls the format of the data to return. See the +Retrieve a User endpoint for available contexts. ### Response If the client is currently logged in, a 302 Found status is given. The User is available canonically from the URL specified in the Location header. The User entity containing the current User is also returned in the body for -convenience. +convenience. The fields available on the User depend on the `context` parameter. If the client is not logged in, a 401 Unauthorized status is given. From e6badf1499c0bebe3d43e264b91fe22090eed160 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 12:28:44 +1000 Subject: [PATCH 088/515] Slightly reword and rewrap deletion reassign docs --- docs/routes/users.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/routes/users.md b/docs/routes/users.md index 0c4f4ed15e..e2aa0c6cb8 100644 --- a/docs/routes/users.md +++ b/docs/routes/users.md @@ -138,7 +138,10 @@ set to true, the user will not be able to be restored. Default is false. (boolean) #### `reassign` -The `reassign` parameter controls whether the deleted user's content is reassigned to a new User or not. By default, this is set to null, indicating that the deleted user's content will not be reassigned. +The `reassign` parameter controls whether the deleted user's content is +reassigned to a new User or not. If set to `null`, the deleted user's content +will not be reassigned. + Default is null. (integer) From 997fe4c6601e845489b63c754c581da3998f7724 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 12:30:00 +1000 Subject: [PATCH 089/515] Add missing underscore to WP_User_Query --- docs/routes/users.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/routes/users.md b/docs/routes/users.md index e2aa0c6cb8..6265cc42bb 100644 --- a/docs/routes/users.md +++ b/docs/routes/users.md @@ -18,7 +18,7 @@ The parameter should be an array of the following key/value pairs: * `number` - Number of users to retrieve, use `-1` for all users. Default is set by the site. (integer) * `offset` - Number of posts to skip. Default is 0. (integer) -* `orderby` - Parameter to search by, as per [WP User_Query](https://codex.wordpress.org/Class_Reference/WP_User_Query#Order_.26_Orderby_Parameters). +* `orderby` - Parameter to search by, as per [`WP_User_Query`](https://codex.wordpress.org/Class_Reference/WP_User_Query#Order_.26_Orderby_Parameters). Default is "user_login". (string) * `order` - Order to sort by. Default is "ASC". (string, "ASC" or "DESC") * `s` - Keyword to search for. (string) From af28252ef1d9df8ec0f38629fb9e141c057bf839 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 12:39:00 +1000 Subject: [PATCH 090/515] Document the /posts/ context parameter --- docs/routes/posts.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index 84bcb5d68a..fb6cafe70a 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -76,11 +76,22 @@ Retrieve a Post GET /posts/ ### Input -#### `fields` -... +#### `context` +The `context` parameter controls the format of the data to return. The following +contexts are available: + +* `view`: The default context. Gives the normal User entity. +* `edit`: Context used for extra fields relevant to updating a user. Includes + the `title_raw`, `content_raw`, `guid_raw` and `post_meta` fields, suitable + for editing the post. +* `parent`: Context used when embedding the response inside another (e.g. post + author). This is intended as a minimal subset of the user data to reduce + response size. Returns the `parent` field as an ID, rather than an embedded + post, to ensure we don't traverse the entire post hierarchy. ### Response -The response is a Post entity containing the requested Post if available. +The response is a Post entity containing the requested Post if available. The +fields available on the Post depend on the `context` parameter. Edit a Post From 7c1543a2bdebb50d8200d6dd05e045a9af30185c Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 12:40:59 +1000 Subject: [PATCH 091/515] Document the /posts context parameter --- docs/routes/posts.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index fb6cafe70a..cb705c42de 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -27,8 +27,11 @@ The parameter should be an array of the following key/value pairs: * `s` - Keyword to search for. (string) -#### `fields` -... +#### `context` +The `context` parameter controls the format of the data to return. See the +Retrieve a Post endpoint for available contexts. + +Default is "view". (string) #### `type` From 14583828e23b31856b28d89c02af591113e0ef88 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 12:43:00 +1000 Subject: [PATCH 092/515] Document multiple post type support for /posts --- docs/routes/posts.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index cb705c42de..4bec16b167 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -35,8 +35,16 @@ Default is "view". (string) #### `type` -The `type` parameter specifies the post type to retrieve. Default is "post". -(string) +The `type` parameter specifies the post type to retrieve. This can either be a +string or an array of types. + +Note that arrays are specified using the `[]` URL syntax. e.g. + +``` +GET /posts?type[]=post&type[]=page +``` + +Default is "post". (string) ### Response From abda0a56a101d55f6884508001045f1a7cda0a4a Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 12:47:21 +1000 Subject: [PATCH 093/515] Note default contexts --- docs/routes/users.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/routes/users.md b/docs/routes/users.md index 6265cc42bb..9cfc11b89a 100644 --- a/docs/routes/users.md +++ b/docs/routes/users.md @@ -75,6 +75,8 @@ contexts are available: author). This is intended as a minimal subset of the user data to reduce response size. Excludes `roles` and `capabilities`. +Default is "view". (string) + ### Response The response is a User entity containing the requested User if available. The fields available on the User depend on the `context` parameter. @@ -93,6 +95,8 @@ know the user's ID. The `context` parameter controls the format of the data to return. See the Retrieve a User endpoint for available contexts. +Default is "view". (string) + ### Response If the client is currently logged in, a 302 Found status is given. The User is available canonically from the URL specified in the Location header. From 557aae30e676e68d7aa3935ba6676792b50b8eda Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 12:56:00 +1000 Subject: [PATCH 094/515] Reword "array" input syntax documentation --- docs/compatibility.md | 70 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index 05d31761d1..00fedbe403 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -7,10 +7,78 @@ entities. URL semantics do not specify how to pass this, however the convention used by PHP and this API is to pass them with the array name concatenated with the key name in square brackets. -For example: +For example, a map of key to value (associative arrays) would look like: filter[post_status]=draft&filter[s]=foo +This is equivalent to the following PHP representation: + +```php +$filter = array( + 'post_status' => 'draft', + 's' => 'foo' +); +``` + +Or in Javascript: + +```js +var filter = { + 'post_status': 'draft', + 's': 'foo', +}; +``` + +For lists (numeric arrays), the same syntax is used but with empty key values: + + type[]=post&type[]=page + +This is equivalent to the following PHP representation: + +```php +$type = array( + 'post', + 'page' +); +``` + +Or in Javascript: + +```js +var type = [ + 'post', + 'page' +]; +``` + +These can be combined; for example, using `post__in`: + + filter[post__in][]=1&filter[post__in][]=4&filter[post__in][]=5 + +This is equivalent to the following PHP representation: + +```php +$filter = array( + 'post__in' => array( + 1, + 4, + 5 + ) +); +``` + +Or in Javascript: + +```js +var filter = { + 'post__in': [ + 1, + 4, + 5 + ] +} +``` + ### JSON data input Some posts allow directly passing JSON data (usually an entity) via the request From 0b6b60f8e81200254ca2459e044d9a3d61ee3d71 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 13:33:06 +1000 Subject: [PATCH 095/515] Add documentation on discovery --- docs/guides/getting-started.md | 55 +++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 8eb83e070f..4743d68120 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -22,21 +22,56 @@ That said, it's not a bad idea to know how to read and write JSON by hand. Checking for the API -------------------- -As our first command, let's go ahead and check the API index. The index tells us -what routes are available, and a short summary about the site. +Before you start using the API, it's a good idea to check that the site itself +supports the API. WordPress is flexible enough to allow disabling the API or +parts thereof, and plugins may add or change parts of the API, so it's always +good to double-check this. + +The easiest way to check this is to send a HEAD request to any page on the site. +Any site with the API enabled will return a `Link` header pointing to the API, +with the link relation set to `https://github.com/WP-API/WP-API`. + +My test site for this documentation is set up at `http://example.com/`, so we +can find the API by sending a HEAD request to this URL: + + curl -I http://example.com/ -The index is always available at the site's address with `/wp-json/` on the -end. Note the trailing slash there; it indicates that we want to access the `/` -route. My test site is set up at `http://example.com/`, so all my routes will -start with `http://example.com/wp-json` followed by the route (which here is -just `/`). +(Uppercase `-I` here sends a HEAD request rather than a GET request, since we +only care about the headers here. I'll strip some irrelevant headers for +this documentation.) + +And we get back: + + HTTP/1.1 200 OK + X-Pingback: http://example.com/wp/xmlrpc.php + Link: ; rel="https://github.com/WP-API/WP-API" + +The `Link` header tells us that the base URL for the API is +`http://example.com/wp-json`. Any routes should be appended to this. For +example, the API index that we'll use in a second is available at the `/` route, +so we append this to the URL to get `http://example.com/wp-json/` +(note the trailing slash). + +For sites without pretty permalinks enabled, this will return something like +`http://example.com/?json_route=` instead; the route should again be appended +directly as a string, giving the API index at `http://example.com/?json_route=/` + +(You can also "discover" the API by looking for the HTML `` in the +`` with the same link relation, or in the RSD along with other WordPress +API information.) + +Checking the API Index +---------------------- +As our next command, let's go ahead and check the API index. The index tells us +what routes are available, and a short summary about the site. -Let's fire off the request: +As we discovered previously, the index is available at the site's address with +`/wp-json/` on the end. Let's fire off the request: curl -i http://example.com/wp-json/ -(By the way, `-i` tells cURL that we want to see the headers as well. I'll strip -some irrelevant ones for this documentation.) +(By the way, `-i` tells cURL that we want to see the headers as well. As before, +I'll strip some irrelevant ones for this documentation.) And here's what we get back: From f71383d5c06af9a166ce0c4cbb268f790c32fdca Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 13:35:04 +1000 Subject: [PATCH 096/515] Syntax highlight JSON examples --- docs/guides/getting-started.md | 418 +++++++++++++++++---------------- 1 file changed, 212 insertions(+), 206 deletions(-) diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 4743d68120..2fd9fc4256 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -78,170 +78,172 @@ And here's what we get back: HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 - { - "name": "My WordPress Site", - "description": "Just another WordPress site", - "URL": "http:\/\/example.com", - "routes": { - "\/": { - "supports": [ - "HEAD", - "GET" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/" - } - }, - "\/posts": { - "supports": [ - "HEAD", - "GET", - "POST" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/posts" - }, - "accepts_json": true - }, - "\/posts\/": { - "supports": [ - "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "accepts_json": true - }, - "\/posts\/\/revisions": { - "supports": [ - "HEAD", - "GET" - ] - }, - "\/posts\/\/comments": { - "supports": [ - "HEAD", - "GET", - "POST" - ], - "accepts_json": true - }, - "\/posts\/\/comments\/": { - "supports": [ - "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "accepts_json": true - }, - "\/posts\/types": { - "supports": [ - "HEAD", - "GET" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/posts\/types" - } - }, - "\/posts\/types\/": { - "supports": [ - "HEAD", - "GET" - ] - }, - "\/posts\/statuses": { - "supports": [ - "HEAD", - "GET" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/posts\/statuses" - } - }, - "\/taxonomies": { - "supports": [ - "HEAD", - "GET" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/taxonomies" - } - }, - "\/taxonomies\/": { - "supports": [ - "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "accepts_json": true - }, - "\/taxonomies\/\/terms": { - "supports": [ - "HEAD", - "GET", - "POST" - ], - "accepts_json": true - }, - "\/taxonomies\/\/terms\/": { - "supports": [ - "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "accepts_json": true - }, - "\/users": { - "supports": [ - "HEAD", - "GET", - "POST" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/users" - }, - "accepts_json": true - }, - "\/users\/me": { - "supports": [ - "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/users\/me" - } +```json +{ + "name": "My WordPress Site", + "description": "Just another WordPress site", + "URL": "http:\/\/example.com", + "routes": { + "\/": { + "supports": [ + "HEAD", + "GET" + ], + "meta": { + "self": "http:\/\/example.com\/wp-json\/" + } + }, + "\/posts": { + "supports": [ + "HEAD", + "GET", + "POST" + ], + "meta": { + "self": "http:\/\/example.com\/wp-json\/posts" }, - "\/users\/": { - "supports": [ - "HEAD", - "GET", - "POST" - ], - "accepts_json": true + "accepts_json": true + }, + "\/posts\/": { + "supports": [ + "HEAD", + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "accepts_json": true + }, + "\/posts\/\/revisions": { + "supports": [ + "HEAD", + "GET" + ] + }, + "\/posts\/\/comments": { + "supports": [ + "HEAD", + "GET", + "POST" + ], + "accepts_json": true + }, + "\/posts\/\/comments\/": { + "supports": [ + "HEAD", + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "accepts_json": true + }, + "\/posts\/types": { + "supports": [ + "HEAD", + "GET" + ], + "meta": { + "self": "http:\/\/example.com\/wp-json\/posts\/types" } }, - "meta": { - "links": { - "help": "https:\/\/github.com\/rmccue\/WP-API", - "profile": "https:\/\/raw.github.com\/rmccue\/WP-API\/master\/docs\/schema.json" + "\/posts\/types\/": { + "supports": [ + "HEAD", + "GET" + ] + }, + "\/posts\/statuses": { + "supports": [ + "HEAD", + "GET" + ], + "meta": { + "self": "http:\/\/example.com\/wp-json\/posts\/statuses" + } + }, + "\/taxonomies": { + "supports": [ + "HEAD", + "GET" + ], + "meta": { + "self": "http:\/\/example.com\/wp-json\/taxonomies" } + }, + "\/taxonomies\/": { + "supports": [ + "HEAD", + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "accepts_json": true + }, + "\/taxonomies\/\/terms": { + "supports": [ + "HEAD", + "GET", + "POST" + ], + "accepts_json": true + }, + "\/taxonomies\/\/terms\/": { + "supports": [ + "HEAD", + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "accepts_json": true + }, + "\/users": { + "supports": [ + "HEAD", + "GET", + "POST" + ], + "meta": { + "self": "http:\/\/example.com\/wp-json\/users" + }, + "accepts_json": true + }, + "\/users\/me": { + "supports": [ + "HEAD", + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "meta": { + "self": "http:\/\/example.com\/wp-json\/users\/me" + } + }, + "\/users\/": { + "supports": [ + "HEAD", + "GET", + "POST" + ], + "accepts_json": true + } + }, + "meta": { + "links": { + "help": "https:\/\/github.com\/rmccue\/WP-API", + "profile": "https:\/\/raw.github.com\/rmccue\/WP-API\/master\/docs\/schema.json" } } +} +``` Wow, that's a lot of data! Let's break it down. @@ -316,58 +318,60 @@ And this time, we get (again trimming headers, you'll have more than this): Last-Modified: Wed, 31 Oct 2012 18:26:17 GMT Link: ; rel="item"; title="Hello world!" - [ - { +```json +[ + { + "ID": 1, + "title": "Hello world!", + "status": "publish", + "type": "post", + "author": { "ID": 1, - "title": "Hello world!", - "status": "publish", - "type": "post", - "author": { + "name": "admin", + "slug": "admin", + "URL": "", + "avatar": "http:\/\/0.gravatar.com\/avatar\/c57c8945079831fa3c19caef02e44614&d=404&r=G", + "meta": { + "links": { + "self": "http:\/\/example.com\/wp-json\/users\/1", + "archives": "http:\/\/example.com\/wp-json\/users\/1\/posts" + } + } + }, + "content": "Welcome to WordPress. This is your first post. Edit or delete it, then start blogging!", + "parent": 0, + "link": "http:\/\/example.com\/2012\/10\/31\/hello-world\/", + "date": "2012-10-31T18:26:17+10:00", + "modified": "2012-10-31T18:26:17+10:00", + "format": "standard", + "terms": { + "category": { "ID": 1, - "name": "admin", - "slug": "admin", - "URL": "", - "avatar": "http:\/\/0.gravatar.com\/avatar\/c57c8945079831fa3c19caef02e44614&d=404&r=G", + "name": "Uncategorized", + "slug": "uncategorized", + "group": 0, + "parent": 0, + "count": 1, "meta": { "links": { - "self": "http:\/\/example.com\/wp-json\/users\/1", - "archives": "http:\/\/example.com\/wp-json\/users\/1\/posts" + "collection": "http:\/\/example.com\/wp-json\/taxonomy\/category", + "self": "http:\/\/example.com\/wp-json\/taxonomy\/category\/terms\/1" } } - }, - "content": "Welcome to WordPress. This is your first post. Edit or delete it, then start blogging!", - "parent": 0, - "link": "http:\/\/example.com\/2012\/10\/31\/hello-world\/", - "date": "2012-10-31T18:26:17+10:00", - "modified": "2012-10-31T18:26:17+10:00", - "format": "standard", - "terms": { - "category": { - "ID": 1, - "name": "Uncategorized", - "slug": "uncategorized", - "group": 0, - "parent": 0, - "count": 1, - "meta": { - "links": { - "collection": "http:\/\/example.com\/wp-json\/taxonomy\/category", - "self": "http:\/\/example.com\/wp-json\/taxonomy\/category\/terms\/1" - } - } - } - }, - "meta": { - "links": { - "self": "http:\/\/example.com\/wp-json\/posts\/1", - "author": "http:\/\/example.com\/wp-json\/users\/1", - "collection": "http:\/\/example.com\/wp-json\/posts", - "replies": "http:\/\/example.com\/wp-json\/posts\/1\/comments", - "version-history": "http:\/\/example.com\/wp-json\/posts\/1\/revisions" - } + } + }, + "meta": { + "links": { + "self": "http:\/\/example.com\/wp-json\/posts\/1", + "author": "http:\/\/example.com\/wp-json\/users\/1", + "collection": "http:\/\/example.com\/wp-json\/posts", + "replies": "http:\/\/example.com\/wp-json\/posts\/1\/comments", + "version-history": "http:\/\/example.com\/wp-json\/posts\/1\/revisions" } } - ] + } +] +``` Hopefully this looks fairly self-explanatory. For a full look at all the fields you can get back from this, take a look at [Working with Posts][] or the @@ -405,10 +409,12 @@ The easiest way to do this is to send a JSON body back to the server with just the fields you want to change. For example, to edit the title and the modification date: - { - "title": "Hello Updated World!", - "modified": "2013-04-01T14:00:00+10:00" - } +```json +{ + "title": "Hello Updated World!", + "modified": "2013-04-01T14:00:00+10:00" +} +``` Save the data as "updated-post.json", then we can send this to the server with the correct headers and authentication. The API uses HTTP Basic authentication: From 468b900fa47a1ce8be67bdcb148bfe3619c7d60d Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 13:37:57 +1000 Subject: [PATCH 097/515] Note that we use pluralised forms of nouns in routes --- docs/guides/getting-started.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 2fd9fc4256..b5d4050330 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -460,6 +460,9 @@ In general, routes follow the same pattern: * PUT updates the entity * DELETE deletes the entity +Note that by convention, the plural form of `` is used in the URL +(e.g. `posts` instead of `post`, `pages` instead of `page`). + Next Steps ---------- You should now be able to understand the basics of accessing and creating data From 889e55100093ce3f1d908a9ace33fe556a9c5c5e Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Mon, 5 May 2014 00:53:38 -0400 Subject: [PATCH 098/515] Add view-revsion context to prepare_post(); exclude version-history and replies links from view-revision context; remove json response and just return struct. --- lib/class-wp-json-posts.php | 51 ++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index a76951cf51..0193f19ca8 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -67,26 +67,19 @@ public function register_routes( $routes ) { public function get_revisions( $id ) { $id = (int) $id; - // Todo: Should everyone be able to read post revisions? - if ( empty( $id ) ) return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); // Todo: Query args filter for wp_get_post_revisions $revisions = wp_get_post_revisions( $id ); - $response = new WP_JSON_Response(); - $struct = array(); - foreach ( $revisions as $revision ) { $post = get_object_vars( $revision ); - $struct[] = $this->prepare_post( $post ); + $struct[] = $this->prepare_post( $post, 'view-revision' ); } - $response->set_data( $struct ); - - return $response; + return $struct; } /** @@ -535,7 +528,7 @@ public function get_post_statuses() { * @access protected * * @param array $post The unprepared post data - * @param array $fields The subset of post type fields to return + * @param string $context The context for the prepared post. (view|view-revision|edit) * @return array The prepared post data */ protected function prepare_post( $post, $context = 'view' ) { @@ -604,7 +597,7 @@ protected function prepare_post( $post, $context = 'view' ) { if ( empty( $post_fields['format'] ) ) $post_fields['format'] = 'standard'; - if ( 'view' === $context && 0 !== $post['post_parent'] ) { + if ( ( 'view' === $context || 'view-revision' == $context ) && 0 !== $post['post_parent'] ) { // Avoid nesting too deeply // This gives post + post-extended + meta for the main post, // post + meta for the parent and just meta for the grandparent @@ -618,22 +611,34 @@ protected function prepare_post( $post, $context = 'view' ) { // Include extended fields. We might come back to this. $_post = array_merge( $_post, $post_fields_extended ); - if ( 'edit' === $context && current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) - $_post = array_merge( $_post, $post_fields_raw ); - elseif ( 'edit' === $context ) - return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); + if ( 'edit' === $context ) { + if ( current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + $_post = array_merge( $_post, $post_fields_raw ); + } else { + return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); + } + } elseif ( 'view-revision' == $context ) { + if ( current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + $_post = array_merge( $_post, $post_fields_raw ); + } else { + return new WP_Error( 'json_cannot_view', __( 'Sorry, you cannot view this revision' ), array( 'status' => 403 ) ); + } + } // Entity meta - $_post['meta'] = array( - 'links' => array( - 'self' => json_url( '/posts/' . $post['ID'] ), - 'author' => json_url( '/users/' . $post['post_author'] ), - 'collection' => json_url( '/posts' ), - 'replies' => json_url( '/posts/' . $post['ID'] . '/comments' ), - 'version-history' => json_url( '/posts/' . $post['ID'] . '/revisions' ), - ), + $links = array( + 'self' => json_url( '/posts/' . $post['ID'] ), + 'author' => json_url( '/users/' . $post['post_author'] ), + 'collection' => json_url( '/posts' ), ); + if ( 'view-revision' != $context ) { + $links['replies'] = json_url( '/posts/' . $post['ID'] . '/comments' ); + $links['version-history'] = json_url( '/posts/' . $post['ID'] . '/revisions' ); + } + + $_post['meta'] = array( 'links' => $links ); + if ( ! empty( $post['post_parent'] ) ) $_post['meta']['links']['up'] = json_url( '/posts/' . (int) $post['post_parent'] ); From 680d8779f25afe1528517c81f3e77aceb429822c Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Mon, 5 May 2014 01:10:59 -0400 Subject: [PATCH 099/515] Add and setup endpoint for deleting a single comment --- lib/class-wp-json-posts.php | 39 ++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 95fa20aeae..7aca2fca55 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -46,7 +46,7 @@ public function register_routes( $routes ) { '/posts/(?P\d+)/comments/(?P\d+)' => array( array( array( $this, 'get_comment' ), WP_JSON_Server::READABLE ), array( '__return_null', WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), - array( '__return_null', WP_JSON_Server::DELETABLE ), + array( array( $this, 'delete_comment' ), WP_JSON_Server::DELETABLE ), ), // Meta-post endpoints @@ -364,6 +364,43 @@ public function delete_post( $id, $force = false ) { } } + /** + * Delete a comment. + * + * @uses wp_delete_comment + * @param int $id Post ID + * @param int $comment Comment ID + * @param boolean $force Skip trash + * @return array + */ + public function delete_comment( $id, $comment, $force = false ) { + $comment = (int) $comment; + + if ( empty( $comment ) ) + return new WP_Error( 'json_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); + + $comment_array = get_comment( $comment, ARRAY_A ); + + if ( empty( $comment_array ) ) + return new WP_Error( 'json_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); + + if ( ! current_user_can( 'edit_comment', $comment_array['comment_ID'] ) ) + return new WP_Error( 'json_user_cannot_delete_comment', __( 'Sorry, you are not allowed to delete this comment.' ), array( 'status' => 401 ) ); + + $result = wp_delete_comment( $comment_array['comment_ID'], $force ); + + if ( ! $result ) + return new WP_Error( 'json_cannot_delete', __( 'The comment cannot be deleted.' ), array( 'status' => 500 ) ); + + if ( $force ) { + return array( 'message' => __( 'Permanently deleted comment' ) ); + } + else { + // TODO: return a HTTP 202 here instead + return array( 'message' => __( 'Deleted comment' ) ); + } + } + /** * Retrieve comments * From 18f39ef59431839baa68243c8e21fbd2a88a1a98 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 16:04:22 +1000 Subject: [PATCH 100/515] Check nonces when using cookie authentication --- plugin.php | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/plugin.php b/plugin.php index 2879de3da3..726ac72a06 100644 --- a/plugin.php +++ b/plugin.php @@ -277,6 +277,73 @@ function json_register_post_type( $post_type, $args ) { } add_action( 'registered_post_type', 'json_register_post_type', 10, 2 ); +/** + * Check for errors when using cookie-based authentication + * + * WordPress' built-in cookie authentication is always active for logged in + * users. However, the API has to check nonces for each request to ensure users + * are not vulnerable to CSRF. + * + * @param WP_Error|mixed $result Error from another authentication handler, null if we should handle it, or another value if not + * @return WP_Error|mixed|boolean + */ +function json_cookie_check_errors( $result ) { + if ( ! empty( $result ) ) { + return $result; + } + + global $wp_json_auth_cookie; + + // Are we using cookie authentication? + // (If we get an auth error, but we're still logged in, another + // authentication must have been used.) + if ( $wp_json_auth_cookie !== true && is_user_logged_in() ) { + return $result; + } + + // Do we have a nonce? + if ( ! isset( $_REQUEST['_wp_json_nonce'] ) ) { + // No nonce at all, so act as if it's an unauthenticated request + wp_set_current_user( 0 ); + return true; + } + + // Check the nonce + $result = wp_verify_nonce( $_REQUEST['_wp_json_nonce'], 'wp_json' ); + if ( ! $result ) { + return new WP_Error( 'json_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) ); + } + + return true; +} +add_filter( 'json_authentication_errors', 'json_cookie_check_errors', 100 ); + +/** + * Collect cookie authentication status + * + * Collects errors from {@see wp_validate_auth_cookie} for use by + * {@see json_cookie_check_errors}. + * + * @param mixed + */ +function json_cookie_collect_status() { + global $wp_json_auth_cookie; + + $status_type = current_action(); + + if ( $status_type !== 'auth_cookie_valid' ) { + $wp_json_auth_cookie = substr( $status_type, 12 ); + return; + } + + $wp_json_auth_cookie = true; +} +add_action( 'auth_cookie_malformed', 'json_cookie_collect_status' ); +add_action( 'auth_cookie_expired', 'json_cookie_collect_status' ); +add_action( 'auth_cookie_bad_username', 'json_cookie_collect_status' ); +add_action( 'auth_cookie_bad_hash', 'json_cookie_collect_status' ); +add_action( 'auth_cookie_valid', 'json_cookie_collect_status' ); + /** * Get URL to a JSON endpoint on a site * From 9ec20057758a4082189cd7dbd27be0d2ceae666c Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 16:34:31 +1000 Subject: [PATCH 101/515] Allow passing nonce via X-WP-Nonce header --- plugin.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugin.php b/plugin.php index 726ac72a06..6654e67b51 100644 --- a/plugin.php +++ b/plugin.php @@ -302,14 +302,22 @@ function json_cookie_check_errors( $result ) { } // Do we have a nonce? - if ( ! isset( $_REQUEST['_wp_json_nonce'] ) ) { + $nonce = null; + if ( isset( $_REQUEST['_wp_json_nonce'] ) ) { + $nonce = $_REQUEST['_wp_json_nonce']; + } + elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) { + $nonce = $_SERVER['HTTP_X_WP_NONCE']; + } + + if ( $nonce === null ) { // No nonce at all, so act as if it's an unauthenticated request wp_set_current_user( 0 ); return true; } // Check the nonce - $result = wp_verify_nonce( $_REQUEST['_wp_json_nonce'], 'wp_json' ); + $result = wp_verify_nonce( $nonce, 'wp_json' ); if ( ! $result ) { return new WP_Error( 'json_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) ); } From 75cdf081a58d799aca047dce8c279b798c1a8c40 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 5 May 2014 16:40:57 +1000 Subject: [PATCH 102/515] Use nonces in the JS implementation --- plugin.php | 2 +- wp-api.js | 27 +++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/plugin.php b/plugin.php index 6654e67b51..ad9fe6c46c 100644 --- a/plugin.php +++ b/plugin.php @@ -208,7 +208,7 @@ function json_api_deactivation( $network_wide ) { */ function json_register_scripts() { wp_register_script( 'wp-api', plugins_url( '/wp-api.js', __FILE__ ), array( 'jquery', 'backbone', 'underscore' ), '0.6', true ); - wp_localize_script( 'wp-api', 'wpApiOptions', array( 'base' => json_url() ) ); + wp_localize_script( 'wp-api', 'wpApiOptions', array( 'base' => json_url(), 'nonce' => wp_create_nonce( 'wp_json' ) ) ); } add_action( 'wp_enqueue_scripts', 'json_register_scripts', -100 ); diff --git a/wp-api.js b/wp-api.js index e5b98ba8e9..41fbdefdad 100644 --- a/wp-api.js +++ b/wp-api.js @@ -67,6 +67,7 @@ api.root = ""; if (wpApiOptions) { api.root = wpApiOptions.base; + api.nonce = wpApiOptions.nonce; } /** @@ -76,12 +77,34 @@ */ api.models = {}; + /** + * wp.api.models.Base + * + * Base model for API objects + */ + api.models.Base = Backbone.Model.extend({ + sync: function (method, model, options) { + var options = options || {}; + + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-WP-Nonce', api.nonce); + + if (beforeSend) { + return beforeSend.apply(this, arguments); + } + }; + + return Backbone.sync(method, model, options); + } + }); + /** * wp.api.models.User * * User Entity model */ - api.models.User = Backbone.Model.extend({ + api.models.User = api.models.Base.extend({ urlRoot: api.root + "/users", defaults: { @@ -107,7 +130,7 @@ * * Post Entity model */ - api.models.Post = Backbone.Model.extend({ + api.models.Post = api.models.Base.extend({ // Technically, these are probably not needed defaults: function () { return { From 7799996ea0ec2def2a8a64502a517a94f20de008 Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Wed, 7 May 2014 17:38:24 -0400 Subject: [PATCH 103/515] Add permalinks step to Quick Setup instructions The `/wp-json/` endpoint is not accessible when following the Quick Setup instructions, because the rewrite rule for the endpoint is not triggered with the default permalink structure. Including a step in the setup instructions to explicitly set a compatible permalink structure should reduce confusion. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9a2fdc2c5c..234ffd7546 100644 --- a/README.md +++ b/README.md @@ -56,14 +56,16 @@ testing environment in a few easy steps: vagrant up ``` -5. Browse to http://vagrant.local/wp/wp-admin/ and activate the WP API plugin +5. Browse to http://vagrant.local/wp/wp-admin/plugins.php and activate the WP API plugin ``` Username: admin Password: password ``` -6. Browse to http://vagrant.local/wp-json/ +6. Browse to http://vagrant.local/wp/wp-admin/options-permalink.php and set the permalink structure to anything other than "Default" + +7. Browse to http://vagrant.local/wp-json/ ### Testing From 805b46a8594c235f4c9239cfd0fb3b62e91d4464 Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Thu, 8 May 2014 07:36:44 -0400 Subject: [PATCH 104/515] Clarify API endpoint with Default permalinks Lines wrapped to 80 chars per PR feedback --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 234ffd7546..40cec1282b 100644 --- a/README.md +++ b/README.md @@ -56,16 +56,19 @@ testing environment in a few easy steps: vagrant up ``` -5. Browse to http://vagrant.local/wp/wp-admin/plugins.php and activate the WP API plugin +5. Browse to http://vagrant.local/wp/wp-admin/plugins.php and activate the WP +API plugin ``` Username: admin Password: password ``` -6. Browse to http://vagrant.local/wp/wp-admin/options-permalink.php and set the permalink structure to anything other than "Default" +6. Browse to http://vagrant.local/wp/wp-admin/options-permalink.php and set +the permalink structure to anything other than "Default" -7. Browse to http://vagrant.local/wp-json/ +7. Browse to http://vagrant.local/wp-json/ (or if the permalink structure is +still "Default," to http://vagrant.local/?json_route=/) ### Testing From 06b9fd46b4ae00a7972e31bc8528860c6c4bffa9 Mon Sep 17 00:00:00 2001 From: Matth_eu Date: Mon, 12 May 2014 00:03:21 +0100 Subject: [PATCH 105/515] Update taxonomy collection to return indexed array --- lib/class-wp-json-taxonomies.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 51412e6943..b5cbcd134e 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -43,7 +43,7 @@ public function get_taxonomies( $type ) { if ( is_wp_error( $tax ) ) continue; - $data[ $tax_type ] = $tax; + $data[] = $tax; } return $data; From 41c56fedb815834ae8ebdf20c03b5b0de3ebff5b Mon Sep 17 00:00:00 2001 From: Matth_eu Date: Mon, 12 May 2014 00:19:20 +0100 Subject: [PATCH 106/515] update get terms to return indexed array --- lib/class-wp-json-taxonomies.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index b5cbcd134e..b130c9e588 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -133,7 +133,7 @@ public function get_terms( $type, $taxonomy ) { $data = array(); foreach ($terms as $term) { - $data[ $term->slug ] = $this->prepare_term( $term, $type ); + $data[] = $this->prepare_term( $term, $type ); } return $data; } From f10cf36e9a4c233927a208ecd3c791ab53b62808 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 11:47:28 +1000 Subject: [PATCH 107/515] Add phpDoc for json_authentication_errors While we're here... --- lib/class-wp-json-server.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 5cfbdfa4f7..d380251f2b 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -96,6 +96,27 @@ class WP_JSON_Server implements WP_JSON_ResponseHandler { * @return WP_Error|null WP_Error indicates unsuccessful login, null indicates successful or no authentication provided */ public function check_authentication() { + /** + * Pass an authentication error to the API + * + * This is used to pass a {@see WP_Error} from an authentication method + * back to the API. + * + * Authentication methods should check first if they're being used, as + * multiple authentication methods can be enabled on a site (cookies, + * HTTP basic auth, OAuth). If the authentication method hooked in is + * not actually being attempted, null should be returned to indicate + * another authentication method should check instead. Similarly, + * callbacks should ensure the value is `null` before checking for + * errors. + * + * A {@see WP_Error} instance can be returned if an error occurs, and + * this should match the format used by API methods internally (that is, + * the `status` data should be used). A callback can return `true` to + * indicate that the authentication method was used, and it succeeded. + * + * @param WP_Error|null|boolean WP_Error if authentication error, null if authentication method wasn't used, true if authentication succeeded + */ return apply_filters( 'json_authentication_errors', null ); } From d0e113c945579892cff855de2d2eaedb820536b8 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 12:01:12 +1000 Subject: [PATCH 108/515] Remove deprecated functions from WP_JSON_ResponseHandler --- lib/class-wp-json-responsehandler.php | 35 --------------------------- 1 file changed, 35 deletions(-) diff --git a/lib/class-wp-json-responsehandler.php b/lib/class-wp-json-responsehandler.php index a3fe4b345b..49ec6d2533 100644 --- a/lib/class-wp-json-responsehandler.php +++ b/lib/class-wp-json-responsehandler.php @@ -1,41 +1,6 @@ Date: Mon, 12 May 2014 12:01:34 +1000 Subject: [PATCH 109/515] Remove error_to_array --- lib/class-wp-json-server.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 5cfbdfa4f7..85eb1866fc 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -129,23 +129,6 @@ protected function error_to_response( $error ) { return $response; } - /** - * Convert an error to an array - * - * This iterates over all error codes and messages to change it into a flat - * array. This enables simpler client behaviour, as it is represented as a - * list in JSON rather than an object/map - * - * @param WP_Error $error - * @return array List of associative arrays with code and message keys - */ - protected function error_to_array( $error ) { - _deprecated_function( 'WP_JSON_Server::error_to_array', 'WPAPI-0.8', 'WP_JSON_Server::error_to_response' ); - - $response = $this->error_to_response( $error ); - return $response->get_data(); - } - /** * Get an appropriate error representation in JSON * From 9efac6292471229e513dbb8ce52bb9dfeed8df62 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 12:04:17 +1000 Subject: [PATCH 110/515] Remove send_status --- lib/class-wp-json-server.php | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 85eb1866fc..db9958aa8f 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -145,7 +145,7 @@ protected function error_to_response( $error ) { */ protected function json_error( $code, $message, $status = null ) { if ( $status ) - $this->send_status( $status ); + $this->set_status( $status ); $error = compact( 'code', 'message' ); return json_encode( array( $error ) ); @@ -466,17 +466,6 @@ public function get_index() { return apply_filters( 'json_index', $available ); } - /** - * Send a HTTP status code - * - * @param int $code HTTP status - */ - public function send_status( $code ) { - _deprecated_function( 'WP_JSON_Server::send_status', 'WPAPI-0.8', 'WP_JSON_Response' ); - - status_header( $code ); - } - /** * Send a HTTP status code * From 0417e3ed3e35212bf3219feb1e49526372f24c42 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 12:05:57 +1000 Subject: [PATCH 111/515] Remove WP_JSON_Server::header --- lib/class-wp-json-server.php | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index db9958aa8f..2697c691b4 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -480,27 +480,6 @@ protected function set_status( $code ) { * * @param string $key Header key * @param string $value Header value - * @param boolean $replace Should we replace the existing header? - */ - public function header( $key, $value, $replace = true ) { - _deprecated_function( 'WP_JSON_Server::header', 'WPAPI-0.8', 'WP_JSON_Response' ); - - // Sanitize as per RFC2616 (Section 4.2): - // Any LWS that occurs between field-content MAY be replaced with a - // single SP before interpreting the field value or forwarding the - // message downstream. - $value = preg_replace( '/\s+/', ' ', $value ); - header( sprintf( '%s: %s', $key, $value ), $replace ); - } - - /** - * Send a HTTP header - * - * This is set via the response object, unlike {@see self::header()}, which - * was called directly before response objects were added. - * - * @param string $key Header key - * @param string $value Header value */ protected function send_header( $key, $value ) { // Sanitize as per RFC2616 (Section 4.2): From 6d76ed425b7dbc38a40c240eba7228c4c45968fb Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 12:06:51 +1000 Subject: [PATCH 112/515] Remove link_header --- lib/class-wp-json-server.php | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 2697c691b4..37ee20b6f3 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -501,31 +501,6 @@ protected function send_headers( $headers ) { } } - /** - * Send a Link header - * - * @todo Make this safe for <>"';, - * @internal The $rel parameter is first, as this looks nicer when sending multiple - * - * @link http://tools.ietf.org/html/rfc5988 - * @link http://www.iana.org/assignments/link-relations/link-relations.xml - * - * @param string $rel Link relation. Either a registered type, or an absolute URL - * @param string $link Target IRI for the link - * @param array $other Other parameters to send, as an assocative array - */ - public function link_header( $rel, $link, $other = array() ) { - _deprecated_function( 'WP_JSON_Server::link_header', 'WPAPI-0.8', 'WP_JSON_Response' ); - - $header = '<' . $link . '>; rel="' . $rel . '"'; - foreach ( $other as $key => $value ) { - if ( 'title' == $key ) - $value = '"' . $value . '"'; - $header .= '; ' . $key . '=' . $value; - } - $this->header( 'Link', $header, false ); - } - /** * Send navigation-related headers for post collections * From 3b050c5be2287e8e564a87df30a20c183790bc38 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 12:07:32 +1000 Subject: [PATCH 113/515] Remove query_navigation_headers --- lib/class-wp-json-server.php | 37 ------------------------------------ 1 file changed, 37 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 37ee20b6f3..e53296e4b7 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -501,43 +501,6 @@ protected function send_headers( $headers ) { } } - /** - * Send navigation-related headers for post collections - * - * @param WP_Query $query - */ - public function query_navigation_headers( $query ) { - _deprecated_function( 'WP_JSON_Server::query_navigation_headers', 'WPAPI-0.8', 'WP_JSON_Response' ); - - $max_page = $query->max_num_pages; - $paged = $query->get('paged'); - - if ( !$paged ) - $paged = 1; - - $nextpage = intval($paged) + 1; - - if ( ! $query->is_single() ) { - if ( $paged > 1 ) { - $request = remove_query_arg( 'page' ); - $request = add_query_arg( 'page', $paged - 1, $request ); - $this->link_header( 'prev', $request ); - } - - if ( $nextpage <= $max_page ) { - $request = remove_query_arg( 'page' ); - $request = add_query_arg( 'page', $nextpage, $request ); - $this->link_header( 'next', $request ); - } - } - - $this->header( 'X-WP-Total', $query->found_posts ); - $this->header( 'X-WP-TotalPages', $max_page ); - - do_action('json_query_navigation_headers', $this, $query); - } - - /** * Retrieve the raw request entity (body) * From f0236a5b55a9b387b71c7b917beb2cd980133002 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 12:07:44 +1000 Subject: [PATCH 114/515] Update documentation --- docs/guides/extending.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guides/extending.md b/docs/guides/extending.md index fad216325e..08fe5df3ee 100644 --- a/docs/guides/extending.md +++ b/docs/guides/extending.md @@ -214,11 +214,11 @@ The data itself can be any JSON-serializable value: an integer, string, array, object or null. In practice, this is almost always an array (for Collections) or an object (for Entities). -Return headers can be set via the `WP_JSON_Server::header()` function, or -similar helper functions (`WP_JSON_Server::send_status()`, -`WP_JSON_Server::link_header()`, `WP_JSON_Server::query_navigation_headers()`) -called on the global `$wp_json_server` object, but should *never* be set via the -direct `header()` or `status_header()` functions. +Return headers can be set by returning an instance of `WP_JSON_Response`, which +includes a `header()` method, as well as similar helper methods +(`set_status()`, `link_header()`, `query_navigation_headers()`) called on the +response object, but should *never* be set via the direct `header()` or +`status_header()` functions. [http-status-codes]: http://en.wikipedia.org/wiki/List_of_HTTP_status_codes From f6385b6348aa22afae465cb7e88e94e90c4edab5 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 12:57:04 +1000 Subject: [PATCH 115/515] Remove semiCamelCased methods --- docs/guides/extending.md | 4 +++- docs/internals/implementation.md | 4 ++-- lib/class-wp-json-media.php | 6 +++--- lib/class-wp-json-posts.php | 17 +---------------- lib/class-wp-json-taxonomies.php | 15 --------------- 5 files changed, 9 insertions(+), 37 deletions(-) diff --git a/docs/guides/extending.md b/docs/guides/extending.md index 08fe5df3ee..8f7a3692a2 100644 --- a/docs/guides/extending.md +++ b/docs/guides/extending.md @@ -257,7 +257,9 @@ built-in types, your registration code should look something like this: // ... } -You will need to implement the getPost, editPost, getPosts, and newPost methods within your new class. Take a look at the WP_JSON_Posts class to see examples of how these methods can be written. +You will need to implement the `get_post`, `edit_post`, `get_posts`, and +`new_post` methods within your new class. Take a look at the `WP_JSON_Posts` +class to see examples of how these methods can be written. Alternatively, use the custom post type base class, which will handle the hooking and more for you: diff --git a/docs/internals/implementation.md b/docs/internals/implementation.md index 278798b96a..f5d93f3cd9 100644 --- a/docs/internals/implementation.md +++ b/docs/internals/implementation.md @@ -3,8 +3,8 @@ Implementation Details Method Naming ------------- -By convention, routing code uses underscore_case for method naming, whereas -endpoints use partialCamelCase. +By convention, all functions and methods use underscore_case, as per the +[WordPress Coding Standards](http://make.wordpress.org/core/handbook/coding-standards/php/). Routing diff --git a/lib/class-wp-json-media.php b/lib/class-wp-json-media.php index 5ae475ee6c..d22218128d 100644 --- a/lib/class-wp-json-media.php +++ b/lib/class-wp-json-media.php @@ -237,7 +237,7 @@ public function upload_attachment( $_files, $_headers ) { } $headers = array( 'Location' => json_url( '/media/' . $id ) ); - return new WP_JSON_Response( $this->getPost( $id, 'edit' ), 201, $headers ); + return new WP_JSON_Response( $this->get_post( $id, 'edit' ), 201, $headers ); } /** @@ -368,7 +368,7 @@ public function preinsert_check( $status, $post, $data ) { // Featured image (pre-verification) if ( ! empty( $data['featured_image'] ) ) { - $thumbnail = $this->getPost( (int) $data['featured_image'], 'child' ); + $thumbnail = $this->get_post( (int) $data['featured_image'], 'child' ); if ( is_wp_error( $thumbnail ) ) { return new WP_Error( 'json_invalid_featured_image', __( 'Invalid featured image.' ), array( 'status' => 400 ) ); } @@ -390,7 +390,7 @@ public function attach_thumbnail( $post, $data, $update ) { } if ( ! empty( $data['featured_image'] ) ) { - // Already verified in preinsertCheck() + // Already verified in preinsert_check() $thumbnail = $this->get_post( $data['featured_image'], 'child' ); set_post_thumbnail( $post['ID'], $thumbnail['ID'] ); } diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 95fa20aeae..bd6e5f67fd 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -658,7 +658,7 @@ protected function prepare_meta( $post_id ) { } /** - * Helper method for wp_newPost and wp_editPost, containing shared logic. + * Helper method for {@see new_post} and {@see edit_post}, containing shared logic. * * @since 3.4.0 * @uses wp_insert_post() @@ -1023,19 +1023,4 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen return apply_filters( 'json_prepare_comment', $data, $comment, $context ); } - - /** - * Magic method used to temporaly deprecate camelcase functions - * - * @param string $name Function name - * @param array $arguments Function arguments - * @return mixed - */ - public function __call($name, $arguments) { - $underscored = strtolower(preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '_$0', $name)); - if ( method_exists( $this, $underscored ) ) { - _deprecated_function( __CLASS__ . '->' . $name, 'WPAPI-0.9', __CLASS__ . '->' . $underscored ); - return call_user_func_array( array( $this, $underscored ), $arguments ); - } - } } diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 51412e6943..490c52fab6 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -210,19 +210,4 @@ protected function prepare_term( $term, $type, $context = 'view' ) { return apply_filters( 'json_prepare_term', $data, $term ); } - - /** - * Magic method used to temporaly deprecate camelcase functions - * - * @param string $name Function name - * @param array $arguments Function arguments - * @return mixed - */ - public function __call($name, $arguments) { - $underscored = strtolower(preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '_$0', $name)); - if ( method_exists( $this, $underscored ) ) { - _deprecated_function( __CLASS__ . '->' . $name, 'WPAPI-0.9', __CLASS__ . '->' . $underscored ); - return call_user_func_array( array( $this, $underscored ), $arguments ); - } - } } From 41c28f6da8b83f77dc696c52ca6bd35a9aeafa91 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 12:59:05 +1000 Subject: [PATCH 116/515] Add serve_request to ResponseHandler --- lib/class-wp-json-responsehandler.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/class-wp-json-responsehandler.php b/lib/class-wp-json-responsehandler.php index 49ec6d2533..a7cace65e1 100644 --- a/lib/class-wp-json-responsehandler.php +++ b/lib/class-wp-json-responsehandler.php @@ -1,6 +1,14 @@ Date: Mon, 12 May 2014 13:11:02 +1000 Subject: [PATCH 117/515] Block serialized meta fields from read/write --- lib/class-wp-json-posts.php | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 467a797dc0..28ce9a8ef6 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -642,6 +642,16 @@ protected function prepare_meta( $post_id ) { // Don't expose protected fields. if ( is_protected_meta( $meta_key ) ) unset( $custom_fields[$meta_key] ); + + // Normalize serialized strings + if ( is_serialized_string( $meta_value ) ) { + $custom_fields[ $meta_key ] = unserialize( $meta_value ); + } + + // Don't expose serialized data + if ( is_serialized( $meta_value ) ) { + unset( $custom_fields[ $meta_key ] ); + } } return apply_filters( 'json_prepare_meta', $custom_fields, $post_id ); @@ -709,18 +719,34 @@ protected function handle_post_meta_action( $post_id, $data ) { protected function maybe_update_post_meta( $post_id, $meta_key, $data ) { $old_data = get_post_meta( $post_id, $meta_key ); - if ( is_object( $old_data ) || is_serialized( $old_data ) ) { - // for now let's not allow updating of arrays, objects or serialized values. + // for now let's not allow updating of arrays, objects or serialized values. + if ( ! $this->is_valid_meta_data( $old_data ) ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); } - if ( is_object( $data ) || is_serialized( $data ) ) { - // for now let's not allow updating of arrays, objects or serialized values. + if ( ! $this->is_valid_meta_data( $data ) ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } return update_post_meta( $post_id, $meta_key, $data ); } + /** + * Check if the data provided is valid data + * + * Excludes serialized data from being sent via the API. + * + * @see https://github.com/WP-API/WP-API/pull/68 + * @param mixed $data Data to be checked + * @return boolean Whether the data is valid or not + */ + protected function is_valid_meta_data( $data ) { + if ( is_array( $data ) || is_object( $data ) || is_serialized( $data ) ) { + return false; + } + + return true; + } + /** * Determines if meta key can be added based on meta data type. * If meta data is allowed by API, the values for the provided meta key is added. @@ -733,7 +759,7 @@ protected function maybe_update_post_meta( $post_id, $meta_key, $data ) { * @return bool|WP_Error */ protected function maybe_add_post_meta( $post_id, $meta_key, $data ) { - if ( is_object( $data ) || is_serialized( $data ) ) { + if ( ! $this->is_valid_meta_data( $data ) ) { // for now let's not allow updating of arrays, objects or serialized values. return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } From 0b5c1faa5ccf6eda5be24b9df0dc6a0befd13ac6 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 13:11:59 +1000 Subject: [PATCH 118/515] Clarify missing meta key code --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 28ce9a8ef6..cffd40e49c 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -685,7 +685,7 @@ protected function handle_post_meta_action( $post_id, $data ) { foreach ( $data as $meta_array ) { if ( empty( $meta_array['key'] ) ) { - return new WP_Error( 'json_post_invalid_action', __( 'Invalid meta action. Missing meta key.' ), array( 'status' => 400 ) ); + return new WP_Error( 'json_post_missing_key', __( 'Missing meta key.' ), array( 'status' => 400 ) ); } $meta_array = wp_parse_args( $meta_array, $meta_array_defaults ); From 2879cae5c8c92c91f60bc2b3ae0814923310e62c Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 13:13:50 +1000 Subject: [PATCH 119/515] Use a switch for post meta actions --- lib/class-wp-json-posts.php | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index cffd40e49c..c1fed9339b 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -690,15 +690,21 @@ protected function handle_post_meta_action( $post_id, $data ) { $meta_array = wp_parse_args( $meta_array, $meta_array_defaults ); - if ( 'update' == $meta_array['action'] ) { - $this->maybe_update_post_meta( $post_id, $meta_array['key'], $meta_array['value'] ); - } elseif ( 'add' == $meta_array['action'] ) { - $this->maybe_add_post_meta( $post_id, $meta_array['key'], $meta_array['value'] ); - } elseif ( 'delete' == $meta_array['action'] ) { - delete_post_meta( $post_id, $meta_array['key'] ); - } else { - return new WP_Error( 'json_post_invalid_action', __( 'Invalid meta action.' ), array( 'status' => 400 ) ); - // continue; + switch ( $meta_array['action'] ) { + case 'update': + $this->maybe_update_post_meta( $post_id, $meta_array['key'], $meta_array['value'] ); + break; + + case 'add': + $this->maybe_add_post_meta( $post_id, $meta_array['key'], $meta_array['value'] ); + break; + + case 'delete': + delete_post_meta( $post_id, $meta_array['key'] ); + break; + + default: + return new WP_Error( 'json_post_invalid_action', __( 'Invalid meta action.' ), array( 'status' => 400 ) ); } } From c72132392b7f21179c1c34ba6fcf38b5903abda4 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 13:17:23 +1000 Subject: [PATCH 120/515] Ensure our meta output matches the expected input --- lib/class-wp-json-posts.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index c1fed9339b..52461337ff 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -638,23 +638,30 @@ protected function prepare_meta( $post_id ) { $custom_fields = (array) get_post_meta( $post_id ); + $meta = array(); foreach ( $custom_fields as $meta_key => $meta_value ) { // Don't expose protected fields. - if ( is_protected_meta( $meta_key ) ) - unset( $custom_fields[$meta_key] ); + if ( is_protected_meta( $meta_key ) ) { + continue; + } // Normalize serialized strings if ( is_serialized_string( $meta_value ) ) { - $custom_fields[ $meta_key ] = unserialize( $meta_value ); + $meta_value = unserialize( $meta_value ); } // Don't expose serialized data if ( is_serialized( $meta_value ) ) { - unset( $custom_fields[ $meta_key ] ); + continue; } + + $meta[] = array( + 'key' => $meta_key, + 'value' => $meta_value, + ); } - return apply_filters( 'json_prepare_meta', $custom_fields, $post_id ); + return apply_filters( 'json_prepare_meta', $meta, $post_id ); } /** From 185d8fa2c3a115901b2fac9a2f98d6a48d7317a9 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 13:26:35 +1000 Subject: [PATCH 121/515] Ensure data is slashed --- lib/class-wp-json-posts.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 52461337ff..52b5810be4 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -740,6 +740,8 @@ protected function maybe_update_post_meta( $post_id, $meta_key, $data ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } + $meta_key = wp_slash( $meta_key ); + $data = wp_slash( $meta_key ); return update_post_meta( $post_id, $meta_key, $data ); } @@ -782,6 +784,8 @@ protected function maybe_add_post_meta( $post_id, $meta_key, $data ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid action provided for meta data.' ), array( 'status' => 400 ) ); } + $meta_key = wp_slash( $meta_key ); + $data = wp_slash( $data ); return add_post_meta( $post_id, $meta_key, $data ); } From 9fca542b063d425c5ddbf7dfee0737ba74b24bfd Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 14:12:59 +1000 Subject: [PATCH 122/515] Use meta ID instead of meta key --- lib/class-wp-json-posts.php | 106 ++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 40 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 52b5810be4..dc4a3c8f8f 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -634,30 +634,37 @@ protected function prepare_excerpt( $excerpt ) { * @return array Custom fields, if exist. */ protected function prepare_meta( $post_id ) { + global $wpdb; $post_id = (int) $post_id; - $custom_fields = (array) get_post_meta( $post_id ); + $table = _get_meta_table( 'post' ); + $results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_key, meta_value FROM $table WHERE post_id = %d", $post_id ) ); $meta = array(); - foreach ( $custom_fields as $meta_key => $meta_value ) { + foreach ( $results as $row ) { + $ID = $row->meta_id; + $key = $row->meta_key; + $value = $row->meta_value; + // Don't expose protected fields. - if ( is_protected_meta( $meta_key ) ) { + if ( is_protected_meta( $key ) ) { continue; } // Normalize serialized strings - if ( is_serialized_string( $meta_value ) ) { - $meta_value = unserialize( $meta_value ); + if ( is_serialized_string( $value ) ) { + $value = unserialize( $value ); } // Don't expose serialized data - if ( is_serialized( $meta_value ) ) { + if ( is_serialized( $value ) ) { continue; } $meta[] = array( - 'key' => $meta_key, - 'value' => $meta_value, + 'ID' => (int) $ID, + 'key' => $key, + 'value' => $value, ); } @@ -669,13 +676,17 @@ protected function prepare_meta( $post_id ) { * { * post_meta : [ * { + * "ID": 42, * "key" : "meta_key", - * "value" : "meta_value", (OPTIONAL) - * "action" : "meta_action" (OPTIONAL, add|update|delete, default=update) + * "value" : "meta_value" * } * ] * } * + * If ID is not specified, the meta value will be created; otherwise, the + * value (and key, if it differs) will be updated. If ID is specified, and + * the key is set to `null`, the data will be deleted. + * * @param array $data * @param int $post_id * @return bool|WP_Error @@ -685,33 +696,34 @@ protected function handle_post_meta_action( $post_id, $data ) { return false; } - $meta_array_defaults = array( - 'action' => 'update', - 'value' => null, - ); - foreach ( $data as $meta_array ) { - if ( empty( $meta_array['key'] ) ) { + if ( ! array_key_exists( 'key', $meta_array ) ) { return new WP_Error( 'json_post_missing_key', __( 'Missing meta key.' ), array( 'status' => 400 ) ); } + if ( ! array_key_exists( 'value', $meta_array ) ) { + return new WP_Error( 'json_post_missing_value', __( 'Missing meta value.' ), array( 'status' => 400 ) ); + } - $meta_array = wp_parse_args( $meta_array, $meta_array_defaults ); - - switch ( $meta_array['action'] ) { - case 'update': - $this->maybe_update_post_meta( $post_id, $meta_array['key'], $meta_array['value'] ); - break; - - case 'add': - $this->maybe_add_post_meta( $post_id, $meta_array['key'], $meta_array['value'] ); - break; - - case 'delete': - delete_post_meta( $post_id, $meta_array['key'] ); - break; + if ( empty( $meta_array['ID'] ) ) { + // Creation + $result = $this->maybe_add_post_meta( $post_id, $meta_array['key'], $meta_array['value'] ); + } + elseif ( empty( $meta_array['key'] ) ) { + // Deletion + $meta_array['ID'] = absint( $meta_array['ID'] ); + $result = delete_metadata_by_mid( 'post', $meta_array['ID'] ); + if ( ! $result ) { + $result = new WP_Error( 'json_meta_could_not_delete', __( 'Could not delete post meta.' ), array( 'status' => 500 ) ); + } + } + else { + // Update + $meta_array['ID'] = absint( $meta_array['ID'] ); + $result = $this->maybe_update_post_meta( $meta_array['ID'], $post_id, $meta_array['key'], $meta_array['value'] ); + } - default: - return new WP_Error( 'json_post_invalid_action', __( 'Invalid meta action.' ), array( 'status' => 400 ) ); + if ( is_wp_error( $result ) ) { + return $result; } } @@ -729,20 +741,27 @@ protected function handle_post_meta_action( $post_id, $data ) { * @param array $data * @return bool|WP_Error */ - protected function maybe_update_post_meta( $post_id, $meta_key, $data ) { - $old_data = get_post_meta( $post_id, $meta_key ); + protected function maybe_update_post_meta( $meta_id, $post_id, $key, $data ) { + $current = get_metadata_by_mid( 'post', $meta_id ); + if ( absint( $current->post_id ) !== $post_id ) { + return new WP_Error( 'json_meta_post_mismatch', __( 'Meta does not belong to this post' ), array( 'status' => 400 ) ); + } // for now let's not allow updating of arrays, objects or serialized values. - if ( ! $this->is_valid_meta_data( $old_data ) ) { + if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); } if ( ! $this->is_valid_meta_data( $data ) ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } - $meta_key = wp_slash( $meta_key ); - $data = wp_slash( $meta_key ); - return update_post_meta( $post_id, $meta_key, $data ); + $key = wp_slash( $key ); + $data = wp_slash( $data ); + if ( ! update_metadata_by_mid( 'post', $meta_id, $data, $key ) ) { + return new WP_Error( 'json_meta_could_not_update', __( 'Could not update post meta.' ), array( 'status' => 500 ) ); + } + + return true; } /** @@ -786,7 +805,11 @@ protected function maybe_add_post_meta( $post_id, $meta_key, $data ) { $meta_key = wp_slash( $meta_key ); $data = wp_slash( $data ); - return add_post_meta( $post_id, $meta_key, $data ); + if ( ! add_post_meta( $post_id, $meta_key, $data ) ) { + return new WP_Error( 'json_meta_could_not_add', __( 'Could not add post meta.' ), array( 'status' => 400 ) ); + } + + return true; } protected function prepare_author( $author ) { @@ -1024,7 +1047,10 @@ protected function insert_post( $data ) { // Post meta if ( ! empty( $data['post_meta'] ) ) { - $this->handle_post_meta_action( $post_ID, $data['post_meta'] ); + $result = $this->handle_post_meta_action( $post_ID, $data['post_meta'] ); + if ( is_wp_error( $result ) ) { + return $result; + } } // Sticky From 162dcdcd437c2ba005c6dada79cd63c41d2ed9a8 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 14:40:46 +1000 Subject: [PATCH 123/515] Allow adding multiple post meta values --- lib/class-wp-json-posts.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index dc4a3c8f8f..17d87e67b1 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -798,11 +798,6 @@ protected function maybe_add_post_meta( $post_id, $meta_key, $data ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } - $old_data = get_post_meta( $post_id, $meta_key ); - if ( ! empty( $old_data ) ) { - return new WP_Error( 'json_post_invalid_action', __( 'Invalid action provided for meta data.' ), array( 'status' => 400 ) ); - } - $meta_key = wp_slash( $meta_key ); $data = wp_slash( $data ); if ( ! add_post_meta( $post_id, $meta_key, $data ) ) { From 0b39d6fd6c99b7eb5d04d52ec929de61ed7bab58 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 17:05:51 +1000 Subject: [PATCH 124/515] Handle comment deletion on CPTs --- lib/class-wp-json-customposttype.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-customposttype.php b/lib/class-wp-json-customposttype.php index b3c942cd51..ed1d1bdc92 100644 --- a/lib/class-wp-json-customposttype.php +++ b/lib/class-wp-json-customposttype.php @@ -99,7 +99,7 @@ public function register_comment_routes( $routes ) { $routes[ $this->base . '/(?P\d+)/comments/(?P\d+)' ] = array( array( array( $this, 'get_comment' ), WP_JSON_Server::READABLE ), array( '__return_null', WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), - array( '__return_null', WP_JSON_Server::DELETABLE ), + array( array( $this, 'delete_comment' ), WP_JSON_Server::DELETABLE ), ); return $routes; } From 95cd274092426734e2e6cb2e4d16733e040d841b Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 17:10:19 +1000 Subject: [PATCH 125/515] Remove placeholder endpoints Doesn't remove revision endpoints, as they'll be handled in #169. --- lib/class-wp-json-customposttype.php | 2 -- lib/class-wp-json-posts.php | 2 -- lib/class-wp-json-taxonomies.php | 3 --- 3 files changed, 7 deletions(-) diff --git a/lib/class-wp-json-customposttype.php b/lib/class-wp-json-customposttype.php index ed1d1bdc92..c1f5ebc3a6 100644 --- a/lib/class-wp-json-customposttype.php +++ b/lib/class-wp-json-customposttype.php @@ -94,11 +94,9 @@ public function register_revision_routes( $routes ) { public function register_comment_routes( $routes ) { $routes[ $this->base . '/(?P\d+)/comments'] = array( array( array( $this, 'get_comments' ), WP_JSON_Server::READABLE ), - array( '__return_null', WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), ); $routes[ $this->base . '/(?P\d+)/comments/(?P\d+)' ] = array( array( array( $this, 'get_comment' ), WP_JSON_Server::READABLE ), - array( '__return_null', WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), array( array( $this, 'delete_comment' ), WP_JSON_Server::DELETABLE ), ); return $routes; diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 7aca2fca55..dc0ecb751c 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -41,11 +41,9 @@ public function register_routes( $routes ) { // Comments '/posts/(?P\d+)/comments' => array( array( array( $this, 'get_comments' ), WP_JSON_Server::READABLE ), - array( '__return_null', WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), ), '/posts/(?P\d+)/comments/(?P\d+)' => array( array( array( $this, 'get_comment' ), WP_JSON_Server::READABLE ), - array( '__return_null', WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), array( array( $this, 'delete_comment' ), WP_JSON_Server::DELETABLE ), ), diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 51412e6943..918ecad75e 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -17,12 +17,9 @@ public function register_routes( $routes ) { ), '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms' => array( array( array( $this, 'get_terms' ), WP_JSON_Server::READABLE ), - array( '__return_null', WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), ), '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms/(?P\w+)' => array( array( array( $this, 'get_term' ), WP_JSON_Server::READABLE ), - array( '__return_null', WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), - array( '__return_null', WP_JSON_Server::DELETABLE ), ), ); return array_merge( $routes, $tax_routes ); From a4e97eb491ebbbeb6f2e9dceec0b6b51333b6fa5 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 17:19:10 +1000 Subject: [PATCH 126/515] Register revisions endpoint for CPTs --- lib/class-wp-json-customposttype.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-customposttype.php b/lib/class-wp-json-customposttype.php index ed1d1bdc92..99ecbb269a 100644 --- a/lib/class-wp-json-customposttype.php +++ b/lib/class-wp-json-customposttype.php @@ -80,7 +80,7 @@ public function register_routes( $routes ) { */ public function register_revision_routes( $routes ) { $routes[ $this->base . '/(?P\d+)/revisions' ] = array( - array( '__return_null', WP_JSON_Server::READABLE ), + array( array( $this, 'get_revisions' ), WP_JSON_Server::READABLE ), ); return $routes; } From 2f21e6dd2b25055631fd4331a0ca1f680ce57308 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 17:36:57 +1000 Subject: [PATCH 127/515] Don't add attachment data to non-attachments --- lib/class-wp-json-media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-media.php b/lib/class-wp-json-media.php index 5ae475ee6c..7965bcbf08 100644 --- a/lib/class-wp-json-media.php +++ b/lib/class-wp-json-media.php @@ -88,7 +88,7 @@ public function get_post( $id, $context = 'view' ) { protected function prepare_post( $post, $context = 'single' ) { $data = parent::prepare_post( $post, $context ); - if ( is_wp_error( $data ) ) + if ( is_wp_error( $data ) || $post['post_type'] !== 'attachment' ) return $data; // $thumbnail_size = current_theme_supports( 'post-thumbnail' ) ? 'post-thumbnail' : 'thumbnail'; From 4847928f12f24d251730701ee81e8789eb130a1b Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 17:37:13 +1000 Subject: [PATCH 128/515] Only add attachment ID on embedded media This avoids recursion for /media (previously attachment -> post -> attachment). --- lib/class-wp-json-media.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/class-wp-json-media.php b/lib/class-wp-json-media.php index 7965bcbf08..bc885b3017 100644 --- a/lib/class-wp-json-media.php +++ b/lib/class-wp-json-media.php @@ -409,6 +409,17 @@ public function add_thumbnail_data( $data, $post, $context ) { return $data; } + // Don't embed too deeply + if ( $context !== 'view' && $context !== 'edit' ) { + $data['featured_image'] = null; + $thumbnail_id = get_post_thumbnail_id( $post['ID'] ); + if ( $thumbnail_id ) { + $data['featured_image'] = absint( $thumbnail_id ); + } + + return $data; + } + // Thumbnail $data['featured_image'] = null; $thumbnail_id = get_post_thumbnail_id( $post['ID'] ); From 44b66a352345f1bca47645a2fc7da31109eb2fd2 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 12 May 2014 17:44:52 +1000 Subject: [PATCH 129/515] Change post parent preparation context to embed This matches the context used in Users. --- lib/class-wp-json-posts.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 3157a8b869..35c4e7b9b9 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -575,7 +575,7 @@ public function get_post_statuses() { * @access protected * * @param array $post The unprepared post data - * @param string $context The context for the prepared post. (view|view-revision|edit) + * @param string $context The context for the prepared post. (view|view-revision|edit|embed) * @return array The prepared post data */ protected function prepare_post( $post, $context = 'view' ) { @@ -649,7 +649,7 @@ protected function prepare_post( $post, $context = 'view' ) { // This gives post + post-extended + meta for the main post, // post + meta for the parent and just meta for the grandparent $parent = get_post( $post['post_parent'], ARRAY_A ); - $post_fields['parent'] = $this->prepare_post( $parent, 'parent' ); + $post_fields['parent'] = $this->prepare_post( $parent, 'embed' ); } // Merge requested $post_fields fields into $_post From ab004a14e342d8e378ebba67859f79f1381973f7 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 22 May 2014 00:07:23 +1000 Subject: [PATCH 130/515] Switch to hybrid approach --- lib/class-wp-json-posts.php | 246 +++++++++++++++++++++++++++++++----- 1 file changed, 216 insertions(+), 30 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 17d87e67b1..3748583309 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -38,6 +38,17 @@ public function register_routes( $routes ) { ), '/posts/(?P\d+)/revisions' => array( '__return_null', WP_JSON_Server::READABLE ), + // Meta + '/posts/(?P\d+)/meta' => array( + array( array( $this, 'get_all_meta' ), WP_JSON_Server::READABLE ), + array( array( $this, 'add_meta' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), + ), + '/posts/(?P\d+)/meta/(?P\d+)' => array( + array( array( $this, 'get_meta' ), WP_JSON_Server::READABLE ), + array( array( $this, 'update_meta' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), + array( array( $this, 'delete_meta' ), WP_JSON_Server::DELETABLE ), + ), + // Comments '/posts/(?P\d+)/comments' => array( array( array( $this, 'get_comments' ), WP_JSON_Server::READABLE ), @@ -540,8 +551,11 @@ protected function prepare_post( $post, $context = 'view' ) { 'title_raw' => $post['post_title'], 'content_raw' => $post['post_content'], 'guid_raw' => $post['guid'], - 'post_meta' => $this->prepare_meta( $post['ID'] ), + 'post_meta' => $this->get_all_meta( $post['ID'] ), ); + if ( is_wp_error( $post_fields_raw['post_meta'] ) ) { + return $post_fields_raw['post_meta']; + } // Dates $timezone = $this->server->get_timezone(); @@ -633,12 +647,27 @@ protected function prepare_excerpt( $excerpt ) { * @param int $post_id Post ID. * @return array Custom fields, if exist. */ - protected function prepare_meta( $post_id ) { - global $wpdb; - $post_id = (int) $post_id; + public function get_all_meta( $id ) { + $id = (int) $id; + + if ( empty( $id ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + $post = get_post( $id, ARRAY_A ); + + $post_type = get_post_type_object( $post['post_type'] ); + if ( empty( $post['ID'] ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); + } + global $wpdb; $table = _get_meta_table( 'post' ); - $results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_key, meta_value FROM $table WHERE post_id = %d", $post_id ) ); + $results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_key, meta_value FROM $table WHERE post_id = %d", $id ) ); $meta = array(); foreach ( $results as $row ) { @@ -668,7 +697,66 @@ protected function prepare_meta( $post_id ) { ); } - return apply_filters( 'json_prepare_meta', $meta, $post_id ); + return apply_filters( 'json_prepare_meta', $meta, $id ); + } + + public function get_meta( $id, $mid ) { + $id = (int) $id; + + if ( empty( $id ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + $post = get_post( $id, ARRAY_A ); + + $post_type = get_post_type_object( $post['post_type'] ); + if ( empty( $post['ID'] ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); + } + + $meta = get_metadata_by_mid( 'post', $mid ); + if ( empty( $meta ) ) { + return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); + } + + if ( absint( $meta->post_id ) !== $id ) { + return new WP_Error( 'json_meta_post_mismatch', __( 'Meta does not belong to this post' ), array( 'status' => 400 ) ); + } + + return $this->prepare_meta( $id, $mid, $meta ); + } + + protected function prepare_meta( $post, $mid, $data ) { + $ID = $data->meta_id; + $key = $data->meta_key; + $value = $data->meta_value; + + // Don't expose protected fields. + if ( is_protected_meta( $key ) ) { + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $key ), array( 'status' => 403 ) ); + } + + // Normalize serialized strings + if ( is_serialized_string( $value ) ) { + $value = unserialize( $value ); + } + + // Don't expose serialized data + if ( is_serialized( $value ) ) { + return new WP_Error( 'json_meta_protected', sprintf( __( '%s contains serialized data.'), $key ), array( 'status' => 403 ) ); + } + + $meta = array( + 'ID' => (int) $ID, + 'key' => $key, + 'value' => $value, + ); + + return apply_filters( 'json_prepare_meta_value', $meta, $post ); } /** @@ -706,20 +794,12 @@ protected function handle_post_meta_action( $post_id, $data ) { if ( empty( $meta_array['ID'] ) ) { // Creation - $result = $this->maybe_add_post_meta( $post_id, $meta_array['key'], $meta_array['value'] ); - } - elseif ( empty( $meta_array['key'] ) ) { - // Deletion - $meta_array['ID'] = absint( $meta_array['ID'] ); - $result = delete_metadata_by_mid( 'post', $meta_array['ID'] ); - if ( ! $result ) { - $result = new WP_Error( 'json_meta_could_not_delete', __( 'Could not delete post meta.' ), array( 'status' => 500 ) ); - } + $result = $this->add_meta( $post_id, $meta_array['key'], $meta_array['value'] ); } else { // Update $meta_array['ID'] = absint( $meta_array['ID'] ); - $result = $this->maybe_update_post_meta( $meta_array['ID'], $post_id, $meta_array['key'], $meta_array['value'] ); + $result = $this->update_meta( $post_id, $meta_array['ID'], $meta_array['key'], $meta_array['value'] ); } if ( is_wp_error( $result ) ) { @@ -741,27 +821,62 @@ protected function handle_post_meta_action( $post_id, $data ) { * @param array $data * @return bool|WP_Error */ - protected function maybe_update_post_meta( $meta_id, $post_id, $key, $data ) { - $current = get_metadata_by_mid( 'post', $meta_id ); - if ( absint( $current->post_id ) !== $post_id ) { + public function update_meta( $id, $mid, $data ) { + $id = (int) $id; + $mid = (int) $mid; + + if ( empty( $id ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + $post = get_post( $id, ARRAY_A ); + + if ( empty( $post['ID'] ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + $post_type = get_post_type_object( $post['post_type'] ); + if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); + } + + $current = get_metadata_by_mid( 'post', $mid ); + if ( empty( $current ) ) { + return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); + } + + if ( absint( $current->post_id ) !== $id ) { return new WP_Error( 'json_meta_post_mismatch', __( 'Meta does not belong to this post' ), array( 'status' => 400 ) ); } + if ( ! array_key_exists( 'key', $data ) ) { + return new WP_Error( 'json_post_missing_key', __( 'Missing meta key.' ), array( 'status' => 400 ) ); + } + if ( ! array_key_exists( 'value', $data ) ) { + return new WP_Error( 'json_post_missing_value', __( 'Missing meta value.' ), array( 'status' => 400 ) ); + } + // for now let's not allow updating of arrays, objects or serialized values. if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); } - if ( ! $this->is_valid_meta_data( $data ) ) { + if ( ! $this->is_valid_meta_data( $data['value'] ) ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } - $key = wp_slash( $key ); - $data = wp_slash( $data ); - if ( ! update_metadata_by_mid( 'post', $meta_id, $data, $key ) ) { + // update_metadata_by_mid will return false if these are equal, so check + // first and pass through + if ( $data['value'] === $current->meta_value ) { + return $this->get_meta( $id, $mid ); + } + + $key = wp_slash( $data['key'] ); + $value = wp_slash( $data['value'] ); + if ( ! update_metadata_by_mid( 'post', $mid, $value, $key ) ) { return new WP_Error( 'json_meta_could_not_update', __( 'Could not update post meta.' ), array( 'status' => 500 ) ); } - return true; + return $this->get_meta( $id, $mid ); } /** @@ -792,19 +907,90 @@ protected function is_valid_meta_data( $data ) { * @param array $data * @return bool|WP_Error */ - protected function maybe_add_post_meta( $post_id, $meta_key, $data ) { - if ( ! $this->is_valid_meta_data( $data ) ) { + public function add_meta( $id, $data ) { + $id = (int) $id; + + if ( empty( $id ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + $post = get_post( $id, ARRAY_A ); + + if ( empty( $post['ID'] ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + $post_type = get_post_type_object( $post['post_type'] ); + if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); + } + + if ( ! array_key_exists( 'key', $data ) ) { + return new WP_Error( 'json_post_missing_key', __( 'Missing meta key.' ), array( 'status' => 400 ) ); + } + if ( ! array_key_exists( 'value', $data ) ) { + return new WP_Error( 'json_post_missing_value', __( 'Missing meta value.' ), array( 'status' => 400 ) ); + } + + if ( ! $this->is_valid_meta_data( $data['value'] ) ) { // for now let's not allow updating of arrays, objects or serialized values. return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } - $meta_key = wp_slash( $meta_key ); - $data = wp_slash( $data ); - if ( ! add_post_meta( $post_id, $meta_key, $data ) ) { + $meta_key = wp_slash( $data['key'] ); + $value = wp_slash( $data['value'] ); + + $result = add_post_meta( $id, $meta_key, $value ); + if ( ! $result ) { return new WP_Error( 'json_meta_could_not_add', __( 'Could not add post meta.' ), array( 'status' => 400 ) ); } - return true; + $response = json_ensure_response( $this->get_meta( $id, $result ) ); + if ( is_wp_error( $response ) ) { + return $response; + } + $response->set_status( 201 ); + $response->header( 'Location', json_url( '/posts/' . $id . '/meta/' . $result ) ); + return $response; + } + + public function delete_meta( $id, $mid ) { + $id = (int) $id; + + if ( empty( $id ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + $post = get_post( $id, ARRAY_A ); + + if ( empty( $post['ID'] ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + $post_type = get_post_type_object( $post['post_type'] ); + if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); + } + + $current = get_metadata_by_mid( 'post', $mid ); + if ( empty( $current ) ) { + return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); + } + + if ( absint( $current->post_id ) !== $id ) { + return new WP_Error( 'json_meta_post_mismatch', __( 'Meta does not belong to this post' ), array( 'status' => 400 ) ); + } + + // for now let's not allow updating of arrays, objects or serialized values. + if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { + return new WP_Error( 'json_post_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); + } + + if ( ! delete_metadata_by_mid( 'post', $mid ) ) { + return new WP_Error( 'json_meta_could_not_add', __( 'Could not delete post meta.' ), array( 'status' => 500 ) ); + } + + return array( 'message' => __( 'Deleted meta' ) );; } protected function prepare_author( $author ) { From 6c9e96c888b6b40f9399f1e19693d9072eabc04a Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 22 May 2014 12:24:21 +1000 Subject: [PATCH 131/515] Only check for error when actually using meta --- lib/class-wp-json-posts.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 3748583309..69eea7ade5 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -553,9 +553,6 @@ protected function prepare_post( $post, $context = 'view' ) { 'guid_raw' => $post['guid'], 'post_meta' => $this->get_all_meta( $post['ID'] ), ); - if ( is_wp_error( $post_fields_raw['post_meta'] ) ) { - return $post_fields_raw['post_meta']; - } // Dates $timezone = $this->server->get_timezone(); @@ -600,10 +597,16 @@ protected function prepare_post( $post, $context = 'view' ) { // Include extended fields. We might come back to this. $_post = array_merge( $_post, $post_fields_extended ); - if ( 'edit' === $context && current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) + if ( 'edit' === $context && current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + if ( is_wp_error( $post_fields_raw['post_meta'] ) ) { + return $post_fields_raw['post_meta']; + } + $_post = array_merge( $_post, $post_fields_raw ); - elseif ( 'edit' === $context ) + } + elseif ( 'edit' === $context ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); + } // Entity meta $_post['meta'] = array( From 52ef717d72fc5cbe95831f13b9ec963d15ad6ebe Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 21 May 2014 22:02:05 -0500 Subject: [PATCH 132/515] Updated Github repo urls in server meta links. --- lib/class-wp-json-server.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 3eb4643018..92cd3707b0 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -450,8 +450,8 @@ public function get_index() { 'authentication' => array(), 'meta' => array( 'links' => array( - 'help' => 'https://github.com/rmccue/WP-API', - 'profile' => 'https://raw.github.com/rmccue/WP-API/master/docs/schema.json', + 'help' => 'https://github.com/WP-API/WP-API', + 'profile' => 'https://raw.github.com/WP-API/WP-API/master/docs/schema.json', ), ), ); From ba04cf6fc9ff062f48ad22ae92cf4e2ee1aa8891 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 21 May 2014 22:03:10 -0500 Subject: [PATCH 133/515] Updated Github repo urls in schema docs. --- docs/schema.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/schema.md b/docs/schema.md index a5693fc53e..51e895deb5 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -155,8 +155,8 @@ value indicating a human-readable documentation page about the API. }, "meta": { "links": { - "help": "https:\/\/github.com\/rmccue\/WP-API", - "profile": "https:\/\/raw.github.com\/rmccue\/WP-API\/master\/docs\/schema.json" + "help": "https:\/\/github.com\/WP-API\/WP-API", + "profile": "https:\/\/raw.github.com\/WP-API\/WP-API\/master\/docs\/schema.json" } } } From a22705e1a5305448bfa296e54719e615daee010f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 23 May 2014 10:56:05 +1000 Subject: [PATCH 134/515] Update meta inline docs --- lib/class-wp-json-posts.php | 54 ++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 69eea7ade5..e862185eb9 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -645,10 +645,8 @@ protected function prepare_excerpt( $excerpt ) { /** * Retrieve custom fields for post. * - * @since 2.5.0 - * - * @param int $post_id Post ID. - * @return array Custom fields, if exist. + * @param int $id Post ID + * @return (array[]|WP_Error) List of meta object data on success, WP_Error otherwise */ public function get_all_meta( $id ) { $id = (int) $id; @@ -703,6 +701,13 @@ public function get_all_meta( $id ) { return apply_filters( 'json_prepare_meta', $meta, $id ); } + /** + * Retrieve custom field object. + * + * @param int $id Post ID + * @param int $mid Metadata ID + * @return array|WP_Error Meta object data on success, WP_Error otherwise + */ public function get_meta( $id, $mid ) { $id = (int) $id; @@ -733,6 +738,14 @@ public function get_meta( $id, $mid ) { return $this->prepare_meta( $id, $mid, $meta ); } + /** + * Prepares meta data for return as an object + * + * @param int $post Post ID + * @param int $mid Metadata ID + * @param stdClass $data Metadata row from database + * @return array|WP_Error Meta object data on success, WP_Error otherwise + */ protected function prepare_meta( $post, $mid, $data ) { $ID = $data->meta_id; $key = $data->meta_key; @@ -814,14 +827,13 @@ protected function handle_post_meta_action( $post_id, $data ) { } /** - * Determines if meta key can be updated based on meta data type. - * If meta data is allowed by API, the values for the provided meta key is updated. + * Add meta to a post * - * @uses update_post_meta() - * - * @param int $post_id - * @param string $meta_key - * @param array $data + * @param int $id Post ID + * @param array $data { + * @type string|null $key Meta key + * @type string|null $key Meta value + * } * @return bool|WP_Error */ public function update_meta( $id, $mid, $data ) { @@ -900,14 +912,13 @@ protected function is_valid_meta_data( $data ) { } /** - * Determines if meta key can be added based on meta data type. - * If meta data is allowed by API, the values for the provided meta key is added. - * - * @uses add_post_meta() + * Add meta to a post * - * @param int $post_id - * @param string $meta_key - * @param array $data + * @param int $id Post ID + * @param array $data { + * @type string|null $key Meta key + * @type string|null $key Meta value + * } * @return bool|WP_Error */ public function add_meta( $id, $data ) { @@ -957,6 +968,13 @@ public function add_meta( $id, $data ) { return $response; } + /** + * Delete meta from a post + * + * @param int $id Post ID + * @param int $mid Metadata ID + * @return array|WP_Error Message on success, WP_Error otherwise + */ public function delete_meta( $id, $mid ) { $id = (int) $id; From 56db01ac3e1a6369ab101b9f7ed98def6f9c28e3 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 23 May 2014 10:56:24 +1000 Subject: [PATCH 135/515] Allow blank key or value --- lib/class-wp-json-posts.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index e862185eb9..b1b2075dd9 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -865,10 +865,10 @@ public function update_meta( $id, $mid, $data ) { } if ( ! array_key_exists( 'key', $data ) ) { - return new WP_Error( 'json_post_missing_key', __( 'Missing meta key.' ), array( 'status' => 400 ) ); + $data['key'] = $current->meta_key; } if ( ! array_key_exists( 'value', $data ) ) { - return new WP_Error( 'json_post_missing_value', __( 'Missing meta value.' ), array( 'status' => 400 ) ); + $data['value'] = $current->meta_value; } // for now let's not allow updating of arrays, objects or serialized values. From d0312d4a87b7c45c0783844e2465c1924f17aa5f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 23 May 2014 10:57:44 +1000 Subject: [PATCH 136/515] Don't require meta ID for prepare_meta The ID should be in the meta_id field anyway. --- lib/class-wp-json-posts.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index b1b2075dd9..66e6b27bf8 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -735,18 +735,17 @@ public function get_meta( $id, $mid ) { return new WP_Error( 'json_meta_post_mismatch', __( 'Meta does not belong to this post' ), array( 'status' => 400 ) ); } - return $this->prepare_meta( $id, $mid, $meta ); + return $this->prepare_meta( $id, $meta ); } /** * Prepares meta data for return as an object * * @param int $post Post ID - * @param int $mid Metadata ID * @param stdClass $data Metadata row from database * @return array|WP_Error Meta object data on success, WP_Error otherwise */ - protected function prepare_meta( $post, $mid, $data ) { + protected function prepare_meta( $post, $data ) { $ID = $data->meta_id; $key = $data->meta_key; $value = $data->meta_value; From 683ebf7007d2fb89503bf1ab559ddccd17192916 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 23 May 2014 10:58:40 +1000 Subject: [PATCH 137/515] Use prepare_meta in get_all_meta --- lib/class-wp-json-posts.php | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 66e6b27bf8..494ce5adc9 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -672,30 +672,12 @@ public function get_all_meta( $id ) { $meta = array(); foreach ( $results as $row ) { - $ID = $row->meta_id; - $key = $row->meta_key; - $value = $row->meta_value; - - // Don't expose protected fields. - if ( is_protected_meta( $key ) ) { + $value = $this->prepare_meta( $id, $row ); + if ( is_wp_error( $value ) ) { continue; } - // Normalize serialized strings - if ( is_serialized_string( $value ) ) { - $value = unserialize( $value ); - } - - // Don't expose serialized data - if ( is_serialized( $value ) ) { - continue; - } - - $meta[] = array( - 'ID' => (int) $ID, - 'key' => $key, - 'value' => $value, - ); + $meta[] = $value; } return apply_filters( 'json_prepare_meta', $meta, $id ); From b0e887bf2bda3d41c1cc2f81185cb22d6dd0cae4 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 23 May 2014 11:06:11 +1000 Subject: [PATCH 138/515] Abstract edit permission check --- lib/class-wp-json-posts.php | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 494ce5adc9..e2e3d37999 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -200,6 +200,21 @@ protected function check_read_permission( $post ) { return false; } + /** + * Check if we can edit a post + * @param array $post Post data + * @return boolean Can we edit it? + */ + protected function check_edit_permission( $post ) { + $post_type = get_post_type_object( $post['post_type'] ); + + if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + return false; + } + + return true; + } + /** * Create a new post for any registered post type. * @@ -657,12 +672,11 @@ public function get_all_meta( $id ) { $post = get_post( $id, ARRAY_A ); - $post_type = get_post_type_object( $post['post_type'] ); if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + if ( ! $this->check_edit_permission( $post ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); } @@ -699,12 +713,11 @@ public function get_meta( $id, $mid ) { $post = get_post( $id, ARRAY_A ); - $post_type = get_post_type_object( $post['post_type'] ); if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + if ( ! $this->check_edit_permission( $post ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); } @@ -831,8 +844,7 @@ public function update_meta( $id, $mid, $data ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - $post_type = get_post_type_object( $post['post_type'] ); - if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + if ( ! $this->check_edit_permission( $post ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); } @@ -915,8 +927,7 @@ public function add_meta( $id, $data ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - $post_type = get_post_type_object( $post['post_type'] ); - if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + if ( ! $this->check_edit_permission( $post ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); } @@ -969,8 +980,7 @@ public function delete_meta( $id, $mid ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - $post_type = get_post_type_object( $post['post_type'] ); - if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + if ( ! $this->check_edit_permission( $post ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); } From a13ca3a6b4e54764e1bf641fe0de229d55264020 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 23 May 2014 11:13:32 +1000 Subject: [PATCH 139/515] Don't double-check in handle_post_meta_action These cases are handled by the underlying add_meta/update_meta, so there's no need for them here. --- lib/class-wp-json-posts.php | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index e2e3d37999..b5edef3b85 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -790,26 +790,14 @@ protected function prepare_meta( $post, $data ) { * @return bool|WP_Error */ protected function handle_post_meta_action( $post_id, $data ) { - if ( empty( $post_id ) || empty( $data ) || ! is_array( $data ) ) { - return false; - } - foreach ( $data as $meta_array ) { - if ( ! array_key_exists( 'key', $meta_array ) ) { - return new WP_Error( 'json_post_missing_key', __( 'Missing meta key.' ), array( 'status' => 400 ) ); - } - if ( ! array_key_exists( 'value', $meta_array ) ) { - return new WP_Error( 'json_post_missing_value', __( 'Missing meta value.' ), array( 'status' => 400 ) ); - } - if ( empty( $meta_array['ID'] ) ) { // Creation - $result = $this->add_meta( $post_id, $meta_array['key'], $meta_array['value'] ); + $result = $this->add_meta( $post_id, $meta_array ); } else { // Update - $meta_array['ID'] = absint( $meta_array['ID'] ); - $result = $this->update_meta( $post_id, $meta_array['ID'], $meta_array['key'], $meta_array['value'] ); + $result = $this->update_meta( $post_id, $meta_array['ID'], $meta_array ); } if ( is_wp_error( $result ) ) { From 230edbe83362980310dd4aeb49fe32cd6bfb539b Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 23 May 2014 11:16:49 +1000 Subject: [PATCH 140/515] Trim some excess whitespace --- lib/class-wp-json-posts.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index b5edef3b85..df4dd4d3cc 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -671,7 +671,6 @@ public function get_all_meta( $id ) { } $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -712,7 +711,6 @@ public function get_meta( $id, $mid ) { } $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -827,7 +825,6 @@ public function update_meta( $id, $mid, $data ) { } $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -910,7 +907,6 @@ public function add_meta( $id, $data ) { } $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -963,7 +959,6 @@ public function delete_meta( $id, $mid ) { } $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } From 2da2dbbf7808e4445c0571189ba201fbf1e2b0c5 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 23 May 2014 15:00:19 +1000 Subject: [PATCH 141/515] Allow updating just the meta key too --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index df4dd4d3cc..abc46210b5 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -859,7 +859,7 @@ public function update_meta( $id, $mid, $data ) { // update_metadata_by_mid will return false if these are equal, so check // first and pass through - if ( $data['value'] === $current->meta_value ) { + if ( $data['value'] === $current->meta_value && $data['key'] === $current->meta_key ) { return $this->get_meta( $id, $mid ); } From b12168ee12f88bbce8511b6166e7acbf2f11a276 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 23 May 2014 15:00:44 +1000 Subject: [PATCH 142/515] Don't leak serialized data on get_meta --- lib/class-wp-json-posts.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index abc46210b5..1f3f36507d 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -685,7 +685,7 @@ public function get_all_meta( $id ) { $meta = array(); foreach ( $results as $row ) { - $value = $this->prepare_meta( $id, $row ); + $value = $this->prepare_meta( $id, $row, true ); if ( is_wp_error( $value ) ) { continue; } @@ -736,9 +736,10 @@ public function get_meta( $id, $mid ) { * * @param int $post Post ID * @param stdClass $data Metadata row from database + * @param boolean $is_serialized Is the value field still serialized? (False indicates the value has been unserialized) * @return array|WP_Error Meta object data on success, WP_Error otherwise */ - protected function prepare_meta( $post, $data ) { + protected function prepare_meta( $post, $data, $is_raw = false ) { $ID = $data->meta_id; $key = $data->meta_key; $value = $data->meta_value; @@ -749,12 +750,12 @@ protected function prepare_meta( $post, $data ) { } // Normalize serialized strings - if ( is_serialized_string( $value ) ) { + if ( $is_raw && is_serialized_string( $value ) ) { $value = unserialize( $value ); } // Don't expose serialized data - if ( is_serialized( $value ) ) { + if ( is_serialized( $value ) || ! is_string( $value ) ) { return new WP_Error( 'json_meta_protected', sprintf( __( '%s contains serialized data.'), $key ), array( 'status' => 403 ) ); } From ad0ba2e72fa2aaada73b3cfae472459536c1f59f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 23 May 2014 23:15:27 +1000 Subject: [PATCH 143/515] Check protected keys for update/add --- lib/class-wp-json-posts.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 1f3f36507d..9c8814d37e 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -857,6 +857,12 @@ public function update_meta( $id, $mid, $data ) { if ( ! $this->is_valid_meta_data( $data['value'] ) ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } + if ( is_protected_meta( $current->meta_key ) ) { + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $current->meta_key ), array( 'status' => 403 ) ); + } + if ( is_protected_meta( $data['key'] ) ) { + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $key ), array( 'status' => 403 ) ); + } // update_metadata_by_mid will return false if these are equal, so check // first and pass through @@ -927,6 +933,9 @@ public function add_meta( $id, $data ) { // for now let's not allow updating of arrays, objects or serialized values. return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } + if ( is_protected_meta( $data['key'] ) ) { + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $data['key'] ), array( 'status' => 403 ) ); + } $meta_key = wp_slash( $data['key'] ); $value = wp_slash( $data['value'] ); From 742d474b6450c18e1ea25c3d7920b05d4dc746fb Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 23 May 2014 23:15:45 +1000 Subject: [PATCH 144/515] Add post meta tests --- tests/test_json_posts_meta.php | 525 +++++++++++++++++++++++++++++++++ 1 file changed, 525 insertions(+) create mode 100644 tests/test_json_posts_meta.php diff --git a/tests/test_json_posts_meta.php b/tests/test_json_posts_meta.php new file mode 100644 index 0000000000..4ddfe2ed4e --- /dev/null +++ b/tests/test_json_posts_meta.php @@ -0,0 +1,525 @@ +user = $this->factory->user->create(); + wp_set_current_user( $this->user ); + $this->user_obj = wp_get_current_user(); + $this->user_obj->add_role('author'); + + $this->fake_server = $this->getMock('WP_JSON_Server'); + $this->endpoint = new WP_JSON_Posts( $this->fake_server ); + } + + protected function assertErrorResponse( $code, $response, $status = null ) { + $this->assertInstanceOf( 'WP_Error', $response ); + $this->assertEquals( $code, $response->get_error_code() ); + + if ( $status !== null ) { + $data = $response->get_error_data(); + $this->assertArrayHasKey( 'status', $data ); + $this->assertEquals( $status, $data['status'] ); + } + } + + public function test_get_meta() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $response = $this->endpoint->get_meta( $post_id, $meta_id ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( $meta_id, $data['ID'] ); + $this->assertEquals( 'testkey', $data['key'] ); + $this->assertEquals( 'testvalue', $data['value'] ); + } + + public function test_get_meta_no_post_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $response = $this->endpoint->get_meta( 0, $meta_id ); + $this->assertErrorResponse( 'json_post_invalid_id', $response, 404 ); + } + + public function test_get_meta_invalid_post_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $response = $this->endpoint->get_meta( -1, $meta_id ); + $this->assertErrorResponse( 'json_post_invalid_id', $response, 404 ); + } + + public function test_get_meta_no_meta_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $response = $this->endpoint->get_meta( $post_id, 0 ); + $this->assertErrorResponse( 'json_meta_invalid_id', $response, 404 ); + } + + public function test_get_meta_invalid_meta_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $response = $this->endpoint->get_meta( $post_id, -1 ); + $this->assertErrorResponse( 'json_meta_invalid_id', $response, 404 ); + } + + public function test_get_meta_protected() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, '_testkey', 'testvalue' ); + + $response = $this->endpoint->get_meta( $post_id, $meta_id ); + $this->assertErrorResponse( 'json_meta_protected', $response, 403 ); + } + + public function test_get_meta_serialized_array() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', array( 'testvalue' => 'test' ) ); + + $response = $this->endpoint->get_meta( $post_id, $meta_id ); + $this->assertErrorResponse( 'json_meta_protected', $response, 403 ); + } + + public function test_get_meta_serialized_object() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', (object) array( 'testvalue' => 'test' ) ); + + $response = $this->endpoint->get_meta( $post_id, $meta_id ); + $this->assertErrorResponse( 'json_meta_protected', $response, 403 ); + } + + public function test_get_meta_unauthenticated() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + wp_set_current_user( 0 ); + + $response = $this->endpoint->get_meta( $post_id, $meta_id ); + $this->assertErrorResponse( 'json_cannot_edit', $response, 403 ); + } + + public function test_get_meta_wrong_post() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $post_id_two = $this->factory->post->create(); + $meta_id_two = add_post_meta( $post_id_two, 'testkey', 'testvalue' ); + + $response = $this->endpoint->get_meta( $post_id_two, $meta_id ); + $this->assertErrorResponse( 'json_meta_post_mismatch', $response, 400 ); + + $response = $this->endpoint->get_meta( $post_id, $meta_id_two ); + $this->assertErrorResponse( 'json_meta_post_mismatch', $response, 400 ); + } + + public function test_add_meta() { + $post_id = $this->factory->post->create(); + $data = array( + 'key' => 'testkey', + 'value' => 'testvalue', + ); + $response = $this->endpoint->add_meta( $post_id, $data ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $meta = get_post_meta( $post_id, 'testkey', false ); + $this->assertNotEmpty( $meta ); + $this->assertCount( 1, $meta ); + $this->assertEquals( 'testvalue', $meta[0] ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'ID', $data ); + $this->assertEquals( 'testkey', $data['key'] ); + $this->assertEquals( 'testvalue', $data['value'] ); + } + + public function test_add_meta_no_post_id() { + $data = array( + 'key' => 'testkey', + 'value' => 'testvalue', + ); + $response = $this->endpoint->add_meta( 0, $data ); + $this->assertErrorResponse( 'json_post_invalid_id', $response, 404 ); + } + + public function test_add_meta_invalid_post_id() { + $data = array( + 'key' => 'testkey', + 'value' => 'testvalue', + ); + $response = $this->endpoint->add_meta( -1, $data ); + $this->assertErrorResponse( 'json_post_invalid_id', $response, 404 ); + } + + public function test_add_meta_no_value() { + $post_id = $this->factory->post->create(); + $data = array( + 'key' => 'testkey', + ); + $response = $this->endpoint->add_meta( $post_id, $data ); + $this->assertErrorResponse( 'json_post_missing_value', $response, 400 ); + } + + public function test_add_meta_no_key() { + $post_id = $this->factory->post->create(); + $data = array( + 'value' => 'testvalue', + ); + $response = $this->endpoint->add_meta( $post_id, $data ); + $this->assertErrorResponse( 'json_post_missing_key', $response, 400 ); + } + + public function test_add_meta_unauthenticated() { + $post_id = $this->factory->post->create(); + $data = array( + 'key' => 'testkey', + 'value' => 'testvalue', + ); + + wp_set_current_user( 0 ); + + $response = $this->endpoint->add_meta( $post_id, $data ); + $this->assertErrorResponse( 'json_cannot_edit', $response, 403 ); + $this->assertEmpty( get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_add_meta_serialized_array() { + $post_id = $this->factory->post->create(); + $data = array( + 'key' => 'testkey', + 'value' => array( 'testvalue1', 'testvalue2' ), + ); + + $response = $this->endpoint->add_meta( $post_id, $data ); + $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); + $this->assertEmpty( get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_add_meta_serialized_object() { + $post_id = $this->factory->post->create(); + $data = array( + 'key' => 'testkey', + 'value' => (object) array( 'testkey1' => 'testvalue1', 'testkey2' => 'testvalue2' ), + ); + + $response = $this->endpoint->add_meta( $post_id, $data ); + $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); + $this->assertEmpty( get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_add_meta_serialized_string() { + $post_id = $this->factory->post->create(); + $data = array( + 'key' => 'testkey', + 'value' => serialize( array( 'testkey1' => 'testvalue1', 'testkey2' => 'testvalue2' ) ), + ); + + $response = $this->endpoint->add_meta( $post_id, $data ); + $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); + $this->assertEmpty( get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_add_meta_failed_get() { + $this->endpoint = $this->getMock('WP_JSON_Posts', array('get_meta'), array( $this->fake_server ) ); + + $test_error = new WP_Error( 'json_test_error', 'Test error' ); + $this->endpoint->expects($this->any())->method('get_meta')->will($this->returnValue($test_error)); + + $post_id = $this->factory->post->create(); + $data = array( + 'key' => 'testkey', + 'value' => 'testvalue', + ); + + $response = $this->endpoint->add_meta( $post_id, $data ); + $this->assertErrorResponse( 'json_test_error', $response ); + } + + public function test_add_meta_protected() { + $post_id = $this->factory->post->create(); + $data = array( + 'key' => '_testkey', + 'value' => 'testvalue', + ); + + $response = $this->endpoint->add_meta( $post_id, $data ); + $this->assertErrorResponse( 'json_meta_protected', $response, 403 ); + $this->assertEmpty( get_post_meta( $post_id, '_testkey' ) ); + } + + public function test_update_meta_value() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array( + 'value' => 'testnewvalue', + ); + $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( $meta_id, $data['ID'] ); + $this->assertEquals( 'testkey', $data['key'] ); + $this->assertEquals( 'testnewvalue', $data['value'] ); + + $meta = get_post_meta( $post_id, 'testkey', false ); + $this->assertNotEmpty( $meta ); + $this->assertCount( 1, $meta ); + $this->assertEquals( 'testnewvalue', $meta[0] ); + } + + public function test_update_meta_key() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array( + 'key' => 'testnewkey', + ); + $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( $meta_id, $data['ID'] ); + $this->assertEquals( 'testnewkey', $data['key'] ); + $this->assertEquals( 'testvalue', $data['value'] ); + + $meta = get_post_meta( $post_id, 'testnewkey', false ); + $this->assertNotEmpty( $meta ); + $this->assertCount( 1, $meta ); + $this->assertEquals( 'testvalue', $meta[0] ); + + // Ensure it was actually renamed, not created + $meta = get_post_meta( $post_id, 'testkey', false ); + $this->assertEmpty( $meta ); + } + + public function test_update_meta_key_and_value() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array( + 'key' => 'testnewkey', + 'value' => 'testnewvalue', + ); + $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( $meta_id, $data['ID'] ); + $this->assertEquals( 'testnewkey', $data['key'] ); + $this->assertEquals( 'testnewvalue', $data['value'] ); + + $meta = get_post_meta( $post_id, 'testnewkey', false ); + $this->assertNotEmpty( $meta ); + $this->assertCount( 1, $meta ); + $this->assertEquals( 'testnewvalue', $meta[0] ); + + // Ensure it was actually renamed, not created + $meta = get_post_meta( $post_id, 'testkey', false ); + $this->assertEmpty( $meta ); + } + + public function test_update_meta_empty() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array(); + $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( $meta_id, $data['ID'] ); + $this->assertEquals( 'testkey', $data['key'] ); + $this->assertEquals( 'testvalue', $data['value'] ); + + $meta = get_post_meta( $post_id, 'testkey', false ); + $this->assertNotEmpty( $meta ); + $this->assertCount( 1, $meta ); + $this->assertEquals( 'testvalue', $meta[0] ); + } + + public function test_update_meta_no_post_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array( + 'key' => 'testnewkey', + 'value' => 'testnewvalue', + ); + $response = $this->endpoint->update_meta( 0, $meta_id, $data ); + $this->assertErrorResponse( 'json_post_invalid_id', $response, 404 ); + } + + public function test_update_meta_invalid_post_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array( + 'key' => 'testnewkey', + 'value' => 'testnewvalue', + ); + $response = $this->endpoint->update_meta( -1, $meta_id, $data ); + $this->assertErrorResponse( 'json_post_invalid_id', $response, 404 ); + } + + public function test_update_meta_no_meta_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array( + 'key' => 'testnewkey', + 'value' => 'testnewvalue', + ); + $response = $this->endpoint->update_meta( $post_id, 0, $data ); + $this->assertErrorResponse( 'json_meta_invalid_id', $response, 404 ); + $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_update_meta_invalid_meta_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array( + 'key' => 'testnewkey', + 'value' => 'testnewvalue', + ); + $response = $this->endpoint->update_meta( $post_id, -1, $data ); + $this->assertErrorResponse( 'json_meta_invalid_id', $response, 404 ); + $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_update_meta_unauthenticated() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + wp_set_current_user( 0 ); + + $data = array( + 'key' => 'testnewkey', + 'value' => 'testnewvalue', + ); + $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); + $this->assertErrorResponse( 'json_cannot_edit', $response, 403 ); + $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_update_meta_wrong_post() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $post_id_two = $this->factory->post->create(); + $meta_id_two = add_post_meta( $post_id_two, 'testkey', 'testvalue' ); + + $data = array( + 'key' => 'testnewkey', + 'value' => 'testnewvalue', + ); + $response = $this->endpoint->update_meta( $post_id_two, $meta_id, $data ); + $this->assertErrorResponse( 'json_meta_post_mismatch', $response, 400 ); + $this->assertEquals( 'testvalue', get_post_meta( $post_id_two, 'testkey' ) ); + + $response = $this->endpoint->get_meta( $post_id, $meta_id_two, $data ); + $this->assertErrorResponse( 'json_meta_post_mismatch', $response, 400 ); + $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_update_meta_serialized_array() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array( + 'value' => array( 'testvalue1', 'testvalue2' ), + ); + $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); + $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); + $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_update_meta_serialized_object() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array( + 'value' => (object) array( 'testkey1' => 'testvalue1', 'testkey2' => 'testvalue2' ), + ); + $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); + $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); + $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_update_meta_serialized_string() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array( + 'value' => serialize( array( 'testkey1' => 'testvalue1', 'testkey2' => 'testvalue2' ) ), + ); + $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); + $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); + $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_update_meta_existing_serialized() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', array( 'testvalue1', 'testvalue2' ) ); + + $data = array( + 'value' => 'testnewvalue', + ); + $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); + $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); + $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_update_meta_protected() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, '_testkey', 'testvalue' ); + + $data = array( + 'value' => 'testnewvalue', + ); + $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); + $this->assertErrorResponse( 'json_meta_protected', $response, 403 ); + $this->assertEquals( 'testvalue', get_post_meta( $post_id, '_testkey' ) ); + } + + public function test_update_meta_protected_new() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array( + 'key' => '_testnewkey', + 'value' => 'testnewvalue', + ); + $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); + $this->assertErrorResponse( 'json_meta_protected', $response, 403 ); + $this->assertEquals( 'testvalue', get_post_meta( $post_id, '_testkey' ) ); + $this->assertEmpty( get_post_meta( $post_id, '_testnewkey' ) ); + } +} From 57cb3ef56e871a20ef45b0bc44a184cb676f8552 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 23 May 2014 11:13:18 -0500 Subject: [PATCH 145/515] Updated documentation for the data parameters for the create and edit post endpoints. --- docs/routes/posts.md | 54 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index 4bec16b167..675537ef2e 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -58,9 +58,7 @@ Create a Post POST /posts ### Input -The supplied data should be a Post object. This data can be submitted via a -regular HTTP multipart body, with Post values set as values to the `data` -parameter, or through a direct JSON body. +The `data` parameter consists of the elements of the Post object to be created. This data can be submitted via a regular HTTP multipart body, with Post values set as values to the `data` parameter, or through a direct JSON body. That is, the following are equivalent: @@ -73,6 +71,28 @@ Content-Type: application/json {"title":"Hello World!","content_raw":"Content","excerpt_raw":"Excerpt"} +The `data` parameter should be an array containing the following key value pairs: + +* `post_title` - Title of the post. (string) *required* +* `content_raw` - Full text of the post. (string) *required* +* `excerpt_raw` - Text for excerpt of the post. (string) *optional* +* `name` - Slug of the post. (string) *optional* +*`status` - Post status of the post: `draft`, `publish`, `pending`, `future`, +`private`, or any custom registered status. If providing a status of `future`, +you must specify a `date` in order for the post to be published as expected. +Default is `draft`. (string) *optional* +* `type` - Post type of the post: `post`, `page`, `link`, `nav_menu_item`, or a any custom registered type. Default is `post`. (string) *optional* * `date` - Time the post was, or should be, published. Date should be an RFC3339 +timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014-01-01T12:20:52Z. Default is the local date and time. (string) *optional* +* `date_gmt` - Time the post was, or should be, published in GMT. Date should be an [RFC3339 timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014-01-01T12:20:52Z. Default is the current GMT date and time. (string) *optional* +* `author` - Author of the post. Author can be provided as a string of the author's ID or as the User object of the author. Default is current user. (object \| string) +*optional* * `password` - Password for protecting the posts. Default is empty +string. (string) *optional* +* `post_parent` - Post ID of the post parent. Default is 0. (integer) *optional* +* `post_format` - Format of the post. Default is `standard`. (string) *optional* +* `menu_order` - The order in which posts specified as the `page` type should appear in supported menus. Default 0. (integer) *optional* +* `comment_status` - Comment status for the post: `open` or +`closed`. Default is the option 'default_comment_status', or 'closed'. (string) *optional* + ### Response On a successful creation, a 201 Created status is given, indicating that the post has been created. The post is available canonically from the URL specified @@ -115,10 +135,30 @@ methods. Both of these methods have the same behaviour as using PUT. It is recommended to use PUT if available to fit with REST convention. ### Input -The supplied data should be a Post object. This data can be submitted via a -regular HTTP multipart body, with Post values set as values to the `data` -parameter, or through a direct JSON body. See the Create Post endpoint for an -example. +The `data` parameter consists of Post ID and the elements of the Post object to be modified. This data can be submitted via a regular HTTP multipart body, with Post values set as values to the `data` parameter, or through a direct JSON body. See the Create Post endpoint for an example. + +The `data` parameter should be an array containing the following key value pairs: + +* `ID` - Unique ID of the post. (integer) *required* +* `post_title` - Title of the post. (string) *optional* +* `content_raw` - Full text of the post. (string) *optional* +* `excerpt_raw` - Text for excerpt of the post. (string) *optional* +* `name` - Slug of the post. (string) *optional* +*`status` - Post status of the post: `draft`, `publish`, `pending`, `future`, +`private`, or any custom registered status. If providing a status of `future`, +you must specify a `date` in order for the post to be published as expected. +Default is `draft`. (string) *optional* +* `type` - Post type of the post: `post`, `page`, `link`, `nav_menu_item`, or a any custom registered type. Default is `post`. (string) *optional* * `date` - Time the post was, or should be, published. Date should be an RFC3339 +timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014-01-01T12:20:52Z. Default is the local date and time. (string) *optional* +* `date_gmt` - Time the post was, or should be, published in GMT. Date should be an [RFC3339 timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014-01-01T12:20:52Z. Default is the current GMT date and time. (string) *optional* +* `author` - Author of the post. Author can be provided as a string of the author's ID or as the User object of the author. Default is current user. (object \| string) +*optional* * `password` - Password for protecting the posts. Default is empty +string. (string) *optional* +* `post_parent` - Post ID of the post parent. Default is 0. (integer) *optional* +* `post_format` - Format of the post. Default is `standard`. (string) *optional* +* `menu_order` - The order in which posts specified as the `page` type should appear in supported menus. Default 0. (integer) *optional* +* `comment_status` - Comment status for the post: `open` or +`closed`. Default is the option 'default_comment_status', or 'closed'. (string) *optional* ### Response On a successful update, a 200 OK status is given, indicating the post has been From 27cccaacb1b2705bbf16bf4bb1c7fc26e92f0409 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 23 May 2014 11:24:51 -0500 Subject: [PATCH 146/515] Testing word wrapping settings. --- docs/routes/posts.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index 675537ef2e..b964ddbf60 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -135,7 +135,10 @@ methods. Both of these methods have the same behaviour as using PUT. It is recommended to use PUT if available to fit with REST convention. ### Input -The `data` parameter consists of Post ID and the elements of the Post object to be modified. This data can be submitted via a regular HTTP multipart body, with Post values set as values to the `data` parameter, or through a direct JSON body. See the Create Post endpoint for an example. +The `data` parameter consists of Post ID and the elements of the Post object +to be modified. This data can be submitted via a regular HTTP multipart body, +with Post values set as values to the `data` parameter, or through a direct +JSON body. See the Create Post endpoint for an example. The `data` parameter should be an array containing the following key value pairs: From 57a8a0583d5f0e4440a9541088d2fe66bdceaf88 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 23 May 2014 11:29:16 -0500 Subject: [PATCH 147/515] Corrected word wrapping on Edit Post data parameters. --- docs/routes/posts.md | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index b964ddbf60..433244af4c 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -151,17 +151,25 @@ The `data` parameter should be an array containing the following key value pairs `private`, or any custom registered status. If providing a status of `future`, you must specify a `date` in order for the post to be published as expected. Default is `draft`. (string) *optional* -* `type` - Post type of the post: `post`, `page`, `link`, `nav_menu_item`, or a any custom registered type. Default is `post`. (string) *optional* * `date` - Time the post was, or should be, published. Date should be an RFC3339 -timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014-01-01T12:20:52Z. Default is the local date and time. (string) *optional* -* `date_gmt` - Time the post was, or should be, published in GMT. Date should be an [RFC3339 timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014-01-01T12:20:52Z. Default is the current GMT date and time. (string) *optional* -* `author` - Author of the post. Author can be provided as a string of the author's ID or as the User object of the author. Default is current user. (object \| string) -*optional* * `password` - Password for protecting the posts. Default is empty -string. (string) *optional* +* `type` - Post type of the post: `post`, `page`, `link`, `nav_menu_item`, or +a any custom registered type. Default is `post`. (string) *optional* +* `date` - Time the post was, or should be, published. Date should be an +RFC3339 timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014-01- +01T12:20:52Z. Default is the local date and time. (string) *optional* +* `date_gmt` - Time the post was, or should be, published in GMT. Date should +be an [RFC3339 timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014 +01-01T12:20:52Z. Default is the current GMT date and time. (string) *optional* +* `author` - Author of the post. Author can be provided as a string of the +author's ID or as the User object of the author. Default is current user. +(object \| string) *optional* +* `password` - Password for protecting the posts. Default is empty string. +(string) *optional* * `post_parent` - Post ID of the post parent. Default is 0. (integer) *optional* * `post_format` - Format of the post. Default is `standard`. (string) *optional* -* `menu_order` - The order in which posts specified as the `page` type should appear in supported menus. Default 0. (integer) *optional* -* `comment_status` - Comment status for the post: `open` or -`closed`. Default is the option 'default_comment_status', or 'closed'. (string) *optional* +* `menu_order` - The order in which posts specified as the `page` type should +appear in supported menus. Default 0. (integer) *optional* +* `comment_status` - Comment status for the post: `open` or`closed`. Default +is the option 'default_comment_status', or 'closed'. (string) *optional* ### Response On a successful update, a 200 OK status is given, indicating the post has been From 1bdae5f699a590f03c008cb2c21ac76cf45800e8 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 23 May 2014 11:35:23 -0500 Subject: [PATCH 148/515] Corrected word wrapping on Create Post data parameters. --- docs/routes/posts.md | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index 433244af4c..5eb76849b9 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -58,7 +58,10 @@ Create a Post POST /posts ### Input -The `data` parameter consists of the elements of the Post object to be created. This data can be submitted via a regular HTTP multipart body, with Post values set as values to the `data` parameter, or through a direct JSON body. +The `data` parameter consists of the elements of the Post object to be +created. This data can be submitted via a regular HTTP multipart body, with +Post values set as values to the `data` parameter, or through a direct JSON +body. That is, the following are equivalent: @@ -81,17 +84,25 @@ The `data` parameter should be an array containing the following key value pairs `private`, or any custom registered status. If providing a status of `future`, you must specify a `date` in order for the post to be published as expected. Default is `draft`. (string) *optional* -* `type` - Post type of the post: `post`, `page`, `link`, `nav_menu_item`, or a any custom registered type. Default is `post`. (string) *optional* * `date` - Time the post was, or should be, published. Date should be an RFC3339 -timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014-01-01T12:20:52Z. Default is the local date and time. (string) *optional* -* `date_gmt` - Time the post was, or should be, published in GMT. Date should be an [RFC3339 timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014-01-01T12:20:52Z. Default is the current GMT date and time. (string) *optional* -* `author` - Author of the post. Author can be provided as a string of the author's ID or as the User object of the author. Default is current user. (object \| string) -*optional* * `password` - Password for protecting the posts. Default is empty -string. (string) *optional* +* `type` - Post type of the post: `post`, `page`, `link`, `nav_menu_item`, or +a any custom registered type. Default is `post`. (string) *optional* +* `date` - Time the post was, or should be, published. Date should be an +RFC3339 timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014-01- +01T12:20:52Z. Default is the local date and time. (string) *optional* +* `date_gmt` - Time the post was, or should be, published in GMT. Date should +be an [RFC3339 timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014 +01-01T12:20:52Z. Default is the current GMT date and time. (string) *optional* +* `author` - Author of the post. Author can be provided as a string of the +author's ID or as the User object of the author. Default is current user. +(object \| string) *optional* +* `password` - Password for protecting the posts. Default is empty string. +(string) *optional* * `post_parent` - Post ID of the post parent. Default is 0. (integer) *optional* * `post_format` - Format of the post. Default is `standard`. (string) *optional* -* `menu_order` - The order in which posts specified as the `page` type should appear in supported menus. Default 0. (integer) *optional* -* `comment_status` - Comment status for the post: `open` or -`closed`. Default is the option 'default_comment_status', or 'closed'. (string) *optional* +* `menu_order` - The order in which posts specified as the `page` type should +appear in supported menus. Default 0. (integer) *optional* +* `comment_status` - Comment status for the post: `open` or`closed`. Default +is the option 'default_comment_status', or 'closed'. (string) *optional* ### Response On a successful creation, a 201 Created status is given, indicating that the From 1eee8b91b739b1d23a6ebf2ef2fa3d48a1bb12bf Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 23 May 2014 14:07:17 -0500 Subject: [PATCH 149/515] Added initial pass at docs for post/id/meta GET and POST routes. Relates to #213 and #207. --- docs/routes/posts.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index 84bcb5d68a..654489aa97 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -124,3 +124,36 @@ later date. If force was set to true, a 200 OK status code will be returned instead, indicating that the post has been permanently deleted. + + +Retrieve Meta for a Post +------------------------ + + GET /posts//meta + +### Response +The response is a Meta entity containing all the post_meta for the specified +Post if available. + + +Create Meta for a Post +------------------------ + + POST /posts//meta + +### Input +The supplied data should be a Meta object. This data can be submitted via a +regular HTTP multipart body, with the Meta key and value set with the `data` +parameter, or through a direct JSON body. + +The `data` parameter should be an array containing the following key value pairs: + +* `key` - The post meta key to be created. (string) *required* +* `value` - The post meta value for the key provided. (string) *required* + +### Response +On a successful creation, a 201 Created status is given, indicating that the +Meta has been created. The post meta is available canonically from the URL +specified in the Location header. + +The new Meta entity is also returned in the body for convienience. From 710aac661732562de740c49afce3a218fdc01436 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 23 May 2014 14:29:16 -0500 Subject: [PATCH 150/515] Added initial pass at docs for post/id/meta/mid GET, PUT and DELETE routes. Relates to #213 and #207. --- docs/routes/posts.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index 654489aa97..5b1a261189 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -157,3 +157,44 @@ Meta has been created. The post meta is available canonically from the URL specified in the Location header. The new Meta entity is also returned in the body for convienience. + + +Retrieve a Meta for a Post +------------------------ + + GET /posts//meta/ + +### Response +The response a Meta entity containing the post_meta for the specified Meta and +Post if available. + + +Edit a Meta for a Post +------------------------ + + PUT /posts//meta/ + +### Input +The supplied data should be a Meta object. This data can be submitted via a +regular HTTP multipart body, with the Meta key and value set with the `data` +parameter, or through a direct JSON body. + +The `data` parameter should be an array containing the following key value pairs: + +* `key` - The post meta key to be updated. (string) *required* +* `value` - The post meta value for the key provided. (string) *required* + +### Response +On a successful update, a 200 OK status is given, indicating the post_meta has +been updated. The updated Meta entity is returned in the body. + + +Delete a Meta for a Post +------------- + + DELETE /posts//meta/ + + +### Response +On successful deletion, a 200 OK status code will be returned, indicating +that the post_meta has been permanently deleted. From f2c62e52767c6b20c8782db13c0e08ecb28e5c1f Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 23 May 2014 21:30:45 -0500 Subject: [PATCH 151/515] Re-ordered post routes to be in CRUD order. --- docs/routes/posts.md | 178 +++++++++++++++++++++---------------------- 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index 5b1a261189..41165fad0a 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -1,6 +1,58 @@ Posts ===== +Create a Post +------------- + + POST /posts + +### Input +The supplied data should be a Post object. This data can be submitted via a +regular HTTP multipart body, with Post values set as values to the `data` +parameter, or through a direct JSON body. + +That is, the following are equivalent: + +Content-Type: application/x-www-form-urlencoded + + data[title]=Hello%20World!&data[content_raw]=Content&data[excerpt_raw]=Excerpt + + +Content-Type: application/json + + {"title":"Hello World!","content_raw":"Content","excerpt_raw":"Excerpt"} + +### Response +On a successful creation, a 201 Created status is given, indicating that the +post has been created. The post is available canonically from the URL specified +in the Location header. + +The new Post entity is also returned in the body for convienience. + + +Create Meta for a Post +------------------------ + + POST /posts//meta + +### Input +The supplied data should be a Meta object. This data can be submitted via a +regular HTTP multipart body, with the Meta key and value set with the `data` +parameter, or through a direct JSON body. + +The `data` parameter should be an array containing the following key value pairs: + +* `key` - The post meta key to be created. (string) *required* +* `value` - The post meta value for the key provided. (string) *required* + +### Response +On a successful creation, a 201 Created status is given, indicating that the +Meta has been created. The post meta is available canonically from the URL +specified in the Location header. + +The new Meta entity is also returned in the body for convienience. + + Retrieve Posts -------------- The Posts endpoint returns a Post Collection containing a subset of the site's @@ -41,46 +93,37 @@ The response is a Post Collection document containing the requested Posts if available. -Create a Post -------------- +Retrieve a Post +--------------- - POST /posts + GET /posts/ ### Input -The supplied data should be a Post object. This data can be submitted via a -regular HTTP multipart body, with Post values set as values to the `data` -parameter, or through a direct JSON body. - -That is, the following are equivalent: - -Content-Type: application/x-www-form-urlencoded +#### `fields` +... - data[title]=Hello%20World!&data[content_raw]=Content&data[excerpt_raw]=Excerpt +### Response +The response is a Post entity containing the requested Post if available. -Content-Type: application/json +Retrieve Meta for a Post +------------------------ - {"title":"Hello World!","content_raw":"Content","excerpt_raw":"Excerpt"} + GET /posts//meta ### Response -On a successful creation, a 201 Created status is given, indicating that the -post has been created. The post is available canonically from the URL specified -in the Location header. - -The new Post entity is also returned in the body for convienience. +The response is a Meta entity containing all the post_meta for the specified +Post if available. -Retrieve a Post ---------------- - - GET /posts/ +Retrieve a Meta for a Post +------------------------ -### Input -#### `fields` -... + GET /posts//meta/ ### Response -The response is a Post entity containing the requested Post if available. +The response a Meta entity containing the post_meta for the specified Meta and +Post if available. Edit a Post @@ -103,43 +146,10 @@ On a successful update, a 200 OK status is given, indicating the post has been updated. The updated Post entity is returned in the body. -Delete a Post -------------- - - DELETE /posts/ - -### Input -#### `force` -The `force` parameter controls whether the post is permanently deleted or not. -By default, this is set to false, indicating that the post will be sent to an -intermediate storage (such as the trash) allowing it to be restored later. If -set to true, the post will not be able to be restored by the user. - -Default is false. (boolean) - -### Response -On successful deletion, a 202 Accepted status code will be returned, indicating -that the post has been moved to the trash for permanent deletion at a -later date. - -If force was set to true, a 200 OK status code will be returned instead, -indicating that the post has been permanently deleted. - - -Retrieve Meta for a Post ------------------------- - - GET /posts//meta - -### Response -The response is a Meta entity containing all the post_meta for the specified -Post if available. - - -Create Meta for a Post +Edit a Meta for a Post ------------------------ - POST /posts//meta + PUT /posts//meta/ ### Input The supplied data should be a Meta object. This data can be submitted via a @@ -148,45 +158,35 @@ parameter, or through a direct JSON body. The `data` parameter should be an array containing the following key value pairs: -* `key` - The post meta key to be created. (string) *required* +* `key` - The post meta key to be updated. (string) *required* * `value` - The post meta value for the key provided. (string) *required* ### Response -On a successful creation, a 201 Created status is given, indicating that the -Meta has been created. The post meta is available canonically from the URL -specified in the Location header. - -The new Meta entity is also returned in the body for convienience. - - -Retrieve a Meta for a Post ------------------------- - - GET /posts//meta/ - -### Response -The response a Meta entity containing the post_meta for the specified Meta and -Post if available. +On a successful update, a 200 OK status is given, indicating the post_meta has +been updated. The updated Meta entity is returned in the body. -Edit a Meta for a Post ------------------------- +Delete a Post +------------- - PUT /posts//meta/ + DELETE /posts/ ### Input -The supplied data should be a Meta object. This data can be submitted via a -regular HTTP multipart body, with the Meta key and value set with the `data` -parameter, or through a direct JSON body. - -The `data` parameter should be an array containing the following key value pairs: +#### `force` +The `force` parameter controls whether the post is permanently deleted or not. +By default, this is set to false, indicating that the post will be sent to an +intermediate storage (such as the trash) allowing it to be restored later. If +set to true, the post will not be able to be restored by the user. -* `key` - The post meta key to be updated. (string) *required* -* `value` - The post meta value for the key provided. (string) *required* +Default is false. (boolean) ### Response -On a successful update, a 200 OK status is given, indicating the post_meta has -been updated. The updated Meta entity is returned in the body. +On successful deletion, a 202 Accepted status code will be returned, indicating +that the post has been moved to the trash for permanent deletion at a +later date. + +If force was set to true, a 200 OK status code will be returned instead, +indicating that the post has been permanently deleted. Delete a Meta for a Post From b403412009eae5dd639e5ded7d2090b3cca1a983 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 23 May 2014 22:01:33 -0500 Subject: [PATCH 152/515] Reordered media endpoints in routes doc to be in CRUD order. --- docs/routes/media.md | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/docs/routes/media.md b/docs/routes/media.md index 98e6477d9e..de462e0c0f 100644 --- a/docs/routes/media.md +++ b/docs/routes/media.md @@ -1,23 +1,6 @@ Media ===== -Get Attachments ---------------- -The Attachments endpoint returns an Attachment collection containing a subset of -the site's attachments. - -This endpoint is an extended version of the Post retrieval endpoint. - - GET /media - -### Input -#### `fields` -... - -### Response -The response is an Attachment entity containing the requested Attachment if -available. - Create an Attachment -------------------- @@ -49,4 +32,22 @@ On a successful creation, a 201 Created status is given, indicating that the attachment has been created. The attachment is available canonically from the URL specified in the Location header. -The new Attachment entity is also returned in the body for convienience. \ No newline at end of file +The new Attachment entity is also returned in the body for convienience. + + +Get Attachments +--------------- +The Attachments endpoint returns an Attachment collection containing a subset of +the site's attachments. + +This endpoint is an extended version of the Post retrieval endpoint. + + GET /media + +### Input +#### `fields` +... + +### Response +The response is an Attachment entity containing the requested Attachment if +available. From 40b656634febad608cb68440df57cc7c5da70902 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 23 May 2014 22:03:09 -0500 Subject: [PATCH 153/515] Reordered user endpoints in routes doc to be in CRUD order. --- docs/routes/users.md | 54 ++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/routes/users.md b/docs/routes/users.md index 9cfc11b89a..e93b566dae 100644 --- a/docs/routes/users.md +++ b/docs/routes/users.md @@ -1,32 +1,6 @@ Users ===== -Retrieve Users --------------- -The Users endpoint returns a User Collection containing a subset of the site's -users. - - GET /users - -### Input -#### `filter` -The `filter` parameter controls the query parameters. It is essentially a subset -of the parameters available to [`WP_User_Query`](http://codex.wordpress.org/Class_Reference/WP_User_Query). - -The parameter should be an array of the following key/value pairs: - -* `number` - Number of users to retrieve, use `-1` for all users. Default - is set by the site. (integer) -* `offset` - Number of posts to skip. Default is 0. (integer) -* `orderby` - Parameter to search by, as per [`WP_User_Query`](https://codex.wordpress.org/Class_Reference/WP_User_Query#Order_.26_Orderby_Parameters). - Default is "user_login". (string) -* `order` - Order to sort by. Default is "ASC". (string, "ASC" or "DESC") -* `s` - Keyword to search for. (string) - -### Response -The response is a User Collection document containing the requested Users if -available. - Create a User ------------- @@ -57,6 +31,33 @@ in the Location header. The new User entity is also returned in the body for convienience. +Retrieve Users +-------------- +The Users endpoint returns a User Collection containing a subset of the site's +users. + + GET /users + +### Input +#### `filter` +The `filter` parameter controls the query parameters. It is essentially a subset +of the parameters available to [`WP_User_Query`](http://codex.wordpress.org/Class_Reference/WP_User_Query). + +The parameter should be an array of the following key/value pairs: + +* `number` - Number of users to retrieve, use `-1` for all users. Default + is set by the site. (integer) +* `offset` - Number of posts to skip. Default is 0. (integer) +* `orderby` - Parameter to search by, as per [`WP_User_Query`](https://codex.wordpress.org/Class_Reference/WP_User_Query#Order_.26_Orderby_Parameters). + Default is "user_login". (string) +* `order` - Order to sort by. Default is "ASC". (string, "ASC" or "DESC") +* `s` - Keyword to search for. (string) + +### Response +The response is a User Collection document containing the requested Users if +available. + + Retrieve a User --------------- @@ -156,4 +157,3 @@ later date. If force was set to true, a 200 OK status code will be returned instead, indicating that the user has been permanently deleted. - From e1f71fef1231a8d8b975b34a36912a29d10bd729 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 16:56:00 +1000 Subject: [PATCH 154/515] Handle testvalue correctly --- tests/test_json_posts_meta.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_json_posts_meta.php b/tests/test_json_posts_meta.php index 4ddfe2ed4e..bb8d36ac7c 100644 --- a/tests/test_json_posts_meta.php +++ b/tests/test_json_posts_meta.php @@ -398,7 +398,7 @@ public function test_update_meta_no_meta_id() { ); $response = $this->endpoint->update_meta( $post_id, 0, $data ); $this->assertErrorResponse( 'json_meta_invalid_id', $response, 404 ); - $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); } public function test_update_meta_invalid_meta_id() { @@ -411,7 +411,7 @@ public function test_update_meta_invalid_meta_id() { ); $response = $this->endpoint->update_meta( $post_id, -1, $data ); $this->assertErrorResponse( 'json_meta_invalid_id', $response, 404 ); - $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); } public function test_update_meta_unauthenticated() { @@ -426,7 +426,7 @@ public function test_update_meta_unauthenticated() { ); $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); $this->assertErrorResponse( 'json_cannot_edit', $response, 403 ); - $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); } public function test_update_meta_wrong_post() { @@ -442,11 +442,11 @@ public function test_update_meta_wrong_post() { ); $response = $this->endpoint->update_meta( $post_id_two, $meta_id, $data ); $this->assertErrorResponse( 'json_meta_post_mismatch', $response, 400 ); - $this->assertEquals( 'testvalue', get_post_meta( $post_id_two, 'testkey' ) ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id_two, 'testkey' ) ); $response = $this->endpoint->get_meta( $post_id, $meta_id_two, $data ); $this->assertErrorResponse( 'json_meta_post_mismatch', $response, 400 ); - $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); } public function test_update_meta_serialized_array() { @@ -458,7 +458,7 @@ public function test_update_meta_serialized_array() { ); $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); - $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); } public function test_update_meta_serialized_object() { @@ -470,7 +470,7 @@ public function test_update_meta_serialized_object() { ); $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); - $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); } public function test_update_meta_serialized_string() { @@ -482,7 +482,7 @@ public function test_update_meta_serialized_string() { ); $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); - $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); } public function test_update_meta_existing_serialized() { @@ -494,7 +494,7 @@ public function test_update_meta_existing_serialized() { ); $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); - $this->assertEquals( 'testvalue', get_post_meta( $post_id, 'testkey' ) ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); } public function test_update_meta_protected() { @@ -506,7 +506,7 @@ public function test_update_meta_protected() { ); $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); $this->assertErrorResponse( 'json_meta_protected', $response, 403 ); - $this->assertEquals( 'testvalue', get_post_meta( $post_id, '_testkey' ) ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, '_testkey' ) ); } public function test_update_meta_protected_new() { @@ -519,7 +519,7 @@ public function test_update_meta_protected_new() { ); $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); $this->assertErrorResponse( 'json_meta_protected', $response, 403 ); - $this->assertEquals( 'testvalue', get_post_meta( $post_id, '_testkey' ) ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, '_testkey' ) ); $this->assertEmpty( get_post_meta( $post_id, '_testnewkey' ) ); } } From b4fbb5e621a66f1153354f90b418153d4a7e5641 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 16:59:28 +1000 Subject: [PATCH 155/515] Use the correct variable for protected meta error --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 9c8814d37e..5f9e85e241 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -861,7 +861,7 @@ public function update_meta( $id, $mid, $data ) { return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $current->meta_key ), array( 'status' => 403 ) ); } if ( is_protected_meta( $data['key'] ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $key ), array( 'status' => 403 ) ); + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $data['key'] ), array( 'status' => 403 ) ); } // update_metadata_by_mid will return false if these are equal, so check From 6dcce7f66efe00f4e1ce03988e75d3895456d279 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 17:00:01 +1000 Subject: [PATCH 156/515] Correct other protected meta tests --- tests/test_json_posts_meta.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_json_posts_meta.php b/tests/test_json_posts_meta.php index bb8d36ac7c..ba99789d94 100644 --- a/tests/test_json_posts_meta.php +++ b/tests/test_json_posts_meta.php @@ -494,7 +494,7 @@ public function test_update_meta_existing_serialized() { ); $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); - $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); + $this->assertEquals( array( array( 'testvalue1', 'testvalue2' ) ), get_post_meta( $post_id, 'testkey' ) ); } public function test_update_meta_protected() { @@ -519,7 +519,7 @@ public function test_update_meta_protected_new() { ); $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); $this->assertErrorResponse( 'json_meta_protected', $response, 403 ); - $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, '_testkey' ) ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); $this->assertEmpty( get_post_meta( $post_id, '_testnewkey' ) ); } } From a16ca21f01eb6f1494f4f87877e9c3def319002b Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 17:35:31 +1000 Subject: [PATCH 157/515] Add delete meta tests --- tests/test_json_posts_meta.php | 125 +++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/tests/test_json_posts_meta.php b/tests/test_json_posts_meta.php index ba99789d94..2b14c223b6 100644 --- a/tests/test_json_posts_meta.php +++ b/tests/test_json_posts_meta.php @@ -522,4 +522,129 @@ public function test_update_meta_protected_new() { $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); $this->assertEmpty( get_post_meta( $post_id, '_testnewkey' ) ); } + + public function test_delete_meta() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $response = $this->endpoint->delete_meta( $post_id, $meta_id ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'message', $data ); + $this->assertNotEmpty( $data['message'] ); + + $meta = get_post_meta( $post_id, 'testkey', false ); + $this->assertEmpty( $meta ); + } + + public function test_delete_meta_no_post_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $response = $this->endpoint->delete_meta( 0, $meta_id ); + $this->assertErrorResponse( 'json_post_invalid_id', $response, 404 ); + + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey', false ) ); + } + + public function test_delete_meta_invalid_post_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $response = $this->endpoint->delete_meta( -1, $meta_id ); + $this->assertErrorResponse( 'json_post_invalid_id', $response, 404 ); + + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey', false ) ); + } + + public function test_delete_meta_no_meta_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $response = $this->endpoint->delete_meta( $post_id, 0 ); + $this->assertErrorResponse( 'json_meta_invalid_id', $response, 404 ); + + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey', false ) ); + } + + public function test_delete_meta_invalid_meta_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $response = $this->endpoint->delete_meta( $post_id, -1 ); + $this->assertErrorResponse( 'json_meta_invalid_id', $response, 404 ); + + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey', false ) ); + } + + public function test_delete_meta_unauthenticated() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + wp_set_current_user( 0 ); + + $response = $this->endpoint->delete_meta( $post_id, $meta_id ); + $this->assertErrorResponse( 'json_cannot_edit', $response, 403 ); + + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey', false ) ); + } + + public function test_delete_meta_wrong_post() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $post_id_two = $this->factory->post->create(); + $meta_id_two = add_post_meta( $post_id_two, 'testkey', 'testvalue' ); + + $response = $this->endpoint->delete_meta( $post_id_two, $meta_id ); + $this->assertErrorResponse( 'json_meta_post_mismatch', $response, 400 ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id_two, 'testkey' ) ); + + $response = $this->endpoint->delete_meta( $post_id, $meta_id_two ); + $this->assertErrorResponse( 'json_meta_post_mismatch', $response, 400 ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_delete_meta_serialized_array() { + $post_id = $this->factory->post->create(); + $value = array( 'testvalue1', 'testvalue2' ); + $meta_id = add_post_meta( $post_id, 'testkey', $value ); + + $response = $this->endpoint->delete_meta( $post_id, $meta_id ); + $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); + $this->assertEquals( array( $value ), get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_delete_meta_serialized_object() { + $post_id = $this->factory->post->create(); + $value = (object) array( 'testkey1' => 'testvalue1', 'testkey2' => 'testvalue2' ); + $meta_id = add_post_meta( $post_id, 'testkey', $value ); + + $response = $this->endpoint->delete_meta( $post_id, $meta_id ); + $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); + $this->assertEquals( array( $value ), get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_delete_meta_serialized_string() { + $post_id = $this->factory->post->create(); + $value = serialize( array( 'testkey1' => 'testvalue1', 'testkey2' => 'testvalue2' ) ); + $meta_id = add_post_meta( $post_id, 'testkey', $value ); + + $response = $this->endpoint->delete_meta( $post_id, $meta_id ); + $this->assertErrorResponse( 'json_post_invalid_action', $response, 400 ); + $this->assertEquals( array( $value ), get_post_meta( $post_id, 'testkey' ) ); + } + + public function test_delete_meta_protected() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, '_testkey', 'testvalue' ); + + $response = $this->endpoint->delete_meta( $post_id, $meta_id ); + $this->assertErrorResponse( 'json_meta_protected', $response, 403 ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, '_testkey' ) ); + } } From a5a934155c0d8885512e891a8b5dd32b1d5377e7 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 17:35:42 +1000 Subject: [PATCH 158/515] Check protected meta before deleting --- lib/class-wp-json-posts.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 5f9e85e241..1cac174f2f 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -990,6 +990,9 @@ public function delete_meta( $id, $mid ) { if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); } + if ( is_protected_meta( $current->meta_key ) ) { + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $current->meta_key ), array( 'status' => 403 ) ); + } if ( ! delete_metadata_by_mid( 'post', $mid ) ) { return new WP_Error( 'json_meta_could_not_add', __( 'Could not delete post meta.' ), array( 'status' => 500 ) ); From 1494e120e9bb62f549d1ab07caeac97f2e5b2205 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 18:25:05 +1000 Subject: [PATCH 159/515] Check that keys are valid for add/update --- lib/class-wp-json-posts.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 1cac174f2f..a8f3cedada 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -850,6 +850,10 @@ public function update_meta( $id, $mid, $data ) { $data['value'] = $current->meta_value; } + if ( empty( $data['key'] ) ) { + return new WP_Error( 'json_meta_invalid_key', __( 'Invalid meta key.' ), array( 'status' => 400 ) ); + } + // for now let's not allow updating of arrays, objects or serialized values. if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); @@ -929,6 +933,10 @@ public function add_meta( $id, $data ) { return new WP_Error( 'json_post_missing_value', __( 'Missing meta value.' ), array( 'status' => 400 ) ); } + if ( empty( $data['key'] ) ) { + return new WP_Error( 'json_meta_invalid_key', __( 'Invalid meta key.' ), array( 'status' => 400 ) ); + } + if ( ! $this->is_valid_meta_data( $data['value'] ) ) { // for now let's not allow updating of arrays, objects or serialized values. return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); From 1e3c6af234201d8a1ea1802c1b43b53de38efc10 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 18:27:49 +1000 Subject: [PATCH 160/515] Test get all meta methods --- tests/test_json_posts_meta.php | 73 ++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/test_json_posts_meta.php b/tests/test_json_posts_meta.php index 2b14c223b6..daea220878 100644 --- a/tests/test_json_posts_meta.php +++ b/tests/test_json_posts_meta.php @@ -126,6 +126,79 @@ public function test_get_meta_wrong_post() { $this->assertErrorResponse( 'json_meta_post_mismatch', $response, 400 ); } + public function test_get_all_meta() { + $post_id = $this->factory->post->create(); + $meta_id_basic = add_post_meta( $post_id, 'testkey', 'testvalue' ); + $meta_id_other1 = add_post_meta( $post_id, 'testotherkey', 'testvalue1' ); + $meta_id_other2 = add_post_meta( $post_id, 'testotherkey', 'testvalue2' ); + $value = array( 'testvalue1', 'testvalue2' ); + $meta_id_serialized = add_post_meta( $post_id, 'testkey', $value ); + $value = (object) array( 'testvalue' => 'test' ); + $meta_id_serialized_object = add_post_meta( $post_id, 'testkey', $value ); + $value = serialize( array( 'testkey1' => 'testvalue1', 'testkey2' => 'testvalue2' ) ); + $meta_id_serialized_string = add_post_meta( $post_id, 'testkey', $value ); + $meta_id_protected = add_post_meta( $post_id, '_testkey', 'testvalue' ); + + $response = $this->endpoint->get_all_meta( $post_id ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertCount( 3, $data ); + + foreach ( $data as $row ) { + $this->assertArrayHasKey( 'ID', $row ); + $this->assertArrayHasKey( 'key', $row ); + $this->assertArrayHasKey( 'value', $row ); + + $this->assertTrue( in_array( $row['ID'], array( $meta_id_basic, $meta_id_other1, $meta_id_other2 ) ) ); + + if ( $row['ID'] === $meta_id_basic ) { + $this->assertEquals( 'testkey', $row['key'] ); + $this->assertEquals( 'testvalue', $row['value'] ); + } + elseif ( $row['ID'] === $meta_id_other1 ) { + $this->assertEquals( 'testotherkey', $row['key'] ); + $this->assertEquals( 'testvalue1', $row['value'] ); + } + elseif ( $row['ID'] === $meta_id_other2 ) { + $this->assertEquals( 'testotherkey', $row['key'] ); + $this->assertEquals( 'testvalue2', $row['value'] ); + } + else { + $this->fail(); + } + } + } + + public function test_get_all_meta_no_post_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $response = $this->endpoint->get_all_meta( 0 ); + $this->assertErrorResponse( 'json_post_invalid_id', $response ); + } + + public function test_get_all_meta_invalid_post_id() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $response = $this->endpoint->get_all_meta( -1 ); + $this->assertErrorResponse( 'json_post_invalid_id', $response ); + } + + public function test_get_all_meta_unauthenticated() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + wp_set_current_user( 0 ); + + $response = $this->endpoint->get_all_meta( $post_id ); + $this->assertErrorResponse( 'json_cannot_edit', $response ); + } + public function test_add_meta() { $post_id = $this->factory->post->create(); $data = array( From e935c2c0fd80fdf60ecc909d4955e759b2a7c35f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 18:28:05 +1000 Subject: [PATCH 161/515] Test invalid keys --- tests/test_json_posts_meta.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_json_posts_meta.php b/tests/test_json_posts_meta.php index daea220878..b8fd0e31a9 100644 --- a/tests/test_json_posts_meta.php +++ b/tests/test_json_posts_meta.php @@ -256,6 +256,16 @@ public function test_add_meta_no_key() { $this->assertErrorResponse( 'json_post_missing_key', $response, 400 ); } + public function test_add_meta_invalid_key() { + $post_id = $this->factory->post->create(); + $data = array( + 'key' => false, + 'value' => 'testvalue', + ); + $response = $this->endpoint->add_meta( $post_id, $data ); + $this->assertErrorResponse( 'json_meta_invalid_key', $response, 400 ); + } + public function test_add_meta_unauthenticated() { $post_id = $this->factory->post->create(); $data = array( @@ -596,6 +606,19 @@ public function test_update_meta_protected_new() { $this->assertEmpty( get_post_meta( $post_id, '_testnewkey' ) ); } + public function test_update_meta_invalid_key() { + $post_id = $this->factory->post->create(); + $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); + + $data = array( + 'key' => false, + 'value' => 'testnewvalue', + ); + $response = $this->endpoint->update_meta( $post_id, $meta_id, $data ); + $this->assertErrorResponse( 'json_meta_invalid_key', $response, 400 ); + $this->assertEquals( array( 'testvalue' ), get_post_meta( $post_id, 'testkey' ) ); + } + public function test_delete_meta() { $post_id = $this->factory->post->create(); $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); From e7f2b0e1df7efc0479bcfe205c794c441bb86c52 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 18:29:28 +1000 Subject: [PATCH 162/515] Move server class load to base --- plugin.php | 3 +-- tests/test_json_server.php | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/plugin.php b/plugin.php index f658155bfe..5f85de9ef1 100644 --- a/plugin.php +++ b/plugin.php @@ -23,6 +23,7 @@ include_once( dirname( __FILE__ ) . '/lib/class-wp-json-datetime.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-responsehandler.php' ); +include_once( dirname( __FILE__ ) . '/lib/class-wp-json-server.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-responseinterface.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-response.php' ); @@ -113,8 +114,6 @@ function json_api_loaded() { if ( empty( $GLOBALS['wp']->query_vars['json_route'] ) ) return; - include_once( dirname( __FILE__ ) . '/lib/class-wp-json-server.php' ); - /** * Whether this is a XMLRPC Request * diff --git a/tests/test_json_server.php b/tests/test_json_server.php index 702960062f..944e95fd6d 100644 --- a/tests/test_json_server.php +++ b/tests/test_json_server.php @@ -24,8 +24,6 @@ function setUp() { parent::setUp(); - include_once( plugin_dir_path( dirname( __FILE__ ) ) . 'lib/class-wp-json-server.php' ); - // Allow for a plugin to insert a different class to handle requests. $wp_json_server_class = apply_filters('wp_json_server_class', 'WP_JSON_Server'); $wp_json_server = new $wp_json_server_class; From 3e3d5a5764c573dcee43da625acd18cc2e420fd4 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 18:32:47 +1000 Subject: [PATCH 163/515] Check plugin location correctly --- tests/bootstrap.php | 2 +- tests/test_json_plugin.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 42cd9587a1..5d8361551f 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -8,7 +8,7 @@ // Activates this plugin in WordPress so it can be tested. $GLOBALS['wp_tests_options'] = array( - 'active_plugins' => array( 'WP-API/plugin.php' ), + 'active_plugins' => array( basename( dirname( dirname( __FILE__ ) ) ) . '/plugin.php' ), ); // If the develop repo location is defined (as WP_DEVELOP_DIR), use that diff --git a/tests/test_json_plugin.php b/tests/test_json_plugin.php index b60eb98377..d63d23c640 100644 --- a/tests/test_json_plugin.php +++ b/tests/test_json_plugin.php @@ -15,7 +15,8 @@ class WP_Test_JSON_Plugin extends WP_UnitTestCase { * The plugin should be installed and activated. */ function test_plugin_activated() { - $this->assertTrue( is_plugin_active( 'WP-API/plugin.php' ) ); + $directory = basename( dirname( dirname( __FILE__ ) ) ); + $this->assertTrue( is_plugin_active( $directory . '/plugin.php' ) ); } /** From e120ab2c63dcd50801245de1a58545e5e560ec80 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 18:45:14 +1000 Subject: [PATCH 164/515] Move meta route documentation after main post docs --- docs/routes/posts.md | 125 ++++++++++++++++++++++--------------------- 1 file changed, 63 insertions(+), 62 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index c974e14efb..1c1c840c2a 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -30,29 +30,6 @@ in the Location header. The new Post entity is also returned in the body for convienience. -Create Meta for a Post ------------------------- - - POST /posts//meta - -### Input -The supplied data should be a Meta object. This data can be submitted via a -regular HTTP multipart body, with the Meta key and value set with the `data` -parameter, or through a direct JSON body. - -The `data` parameter should be an array containing the following key value pairs: - -* `key` - The post meta key to be created. (string) *required* -* `value` - The post meta value for the key provided. (string) *required* - -### Response -On a successful creation, a 201 Created status is given, indicating that the -Meta has been created. The post meta is available canonically from the URL -specified in the Location header. - -The new Meta entity is also returned in the body for convienience. - - Retrieve Posts -------------- The Posts endpoint returns a Post Collection containing a subset of the site's @@ -127,25 +104,6 @@ contexts are available: The response is a Post entity containing the requested Post if available. The fields available on the Post depend on the `context` parameter. -Retrieve Meta for a Post ------------------------- - - GET /posts//meta - -### Response -The response is a Meta entity containing all the post_meta for the specified -Post if available. - - -Retrieve a Meta for a Post ------------------------- - - GET /posts//meta/ - -### Response -The response a Meta entity containing the post_meta for the specified Meta and -Post if available. - Edit a Post ----------- @@ -167,26 +125,6 @@ On a successful update, a 200 OK status is given, indicating the post has been updated. The updated Post entity is returned in the body. -Edit a Meta for a Post ------------------------- - - PUT /posts//meta/ - -### Input -The supplied data should be a Meta object. This data can be submitted via a -regular HTTP multipart body, with the Meta key and value set with the `data` -parameter, or through a direct JSON body. - -The `data` parameter should be an array containing the following key value pairs: - -* `key` - The post meta key to be updated. (string) *required* -* `value` - The post meta value for the key provided. (string) *required* - -### Response -On a successful update, a 200 OK status is given, indicating the post_meta has -been updated. The updated Meta entity is returned in the body. - - Delete a Post ------------- @@ -210,6 +148,69 @@ If force was set to true, a 200 OK status code will be returned instead, indicating that the post has been permanently deleted. +Create Meta for a Post +------------------------ + + POST /posts//meta + +### Input +The supplied data should be a Meta object. This data can be submitted via a +regular HTTP multipart body, with the Meta key and value set with the `data` +parameter, or through a direct JSON body. + +The `data` parameter should be an array containing the following key value pairs: + +* `key` - The post meta key to be created. (string) *required* +* `value` - The post meta value for the key provided. (string) *required* + +### Response +On a successful creation, a 201 Created status is given, indicating that the +Meta has been created. The post meta is available canonically from the URL +specified in the Location header. + +The new Meta entity is also returned in the body for convienience. + + +Retrieve Meta for a Post +------------------------ + + GET /posts//meta + +### Response +The response is a Meta entity containing all the post_meta for the specified +Post if available. + + +Retrieve a Meta for a Post +------------------------ + + GET /posts//meta/ + +### Response +The response a Meta entity containing the post_meta for the specified Meta and +Post if available. + + +Edit a Meta for a Post +------------------------ + + PUT /posts//meta/ + +### Input +The supplied data should be a Meta object. This data can be submitted via a +regular HTTP multipart body, with the Meta key and value set with the `data` +parameter, or through a direct JSON body. + +The `data` parameter should be an array containing the following key value pairs: + +* `key` - The post meta key to be updated. (string) *required* +* `value` - The post meta value for the key provided. (string) *required* + +### Response +On a successful update, a 200 OK status is given, indicating the post_meta has +been updated. The updated Meta entity is returned in the body. + + Delete a Meta for a Post ------------- From 02f9bc82e949217d11bab6d0c3e00824d62dda23 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 20:02:56 +1000 Subject: [PATCH 165/515] Return null from get_date_from_gmt for errors --- lib/class-wp-json-posts.php | 20 ++++++++++++++++---- lib/class-wp-json-server.php | 5 ++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 2351e75e3b..098da83fa5 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -823,18 +823,30 @@ protected function insert_post( $data ) { // Post date if ( ! empty( $data['date'] ) ) { - list( $post['post_date'], $post['post_date_gmt'] ) = $this->server->get_date_with_gmt( $data['date'] ); + $date_data = $this->server->get_date_with_gmt( $data['date'] ); + if ( ! empty( $date_data ) ) { + list( $post['post_date'], $post['post_date_gmt'] ) = $data; + } } elseif ( ! empty( $data['date_gmt'] ) ) { - list( $post['post_date'], $post['post_date_gmt'] ) = $this->server->get_date_with_gmt( $data['date_gmt'], true ); + $date_data = $this->server->get_date_with_gmt( $data['date_gmt'], true ); + if ( ! empty( $date_data ) ) { + list( $post['post_date'], $post['post_date_gmt'] ) = $data; + } } // Post modified if ( ! empty( $data['modified'] ) ) { - list( $post['post_modified'], $post['post_modified_gmt'] ) = $this->server->get_date_with_gmt( $data['modified'] ); + $date_data = $this->server->get_date_with_gmt( $data['modified'] ); + if ( ! empty( $date_data ) ) { + list( $post['post_modified'], $post['post_modified_gmt'] ) = $data; + } } elseif ( ! empty( $data['modified_gmt'] ) ) { - list( $post['post_modified'], $post['post_modified_gmt'] ) = $this->server->get_date_with_gmt( $data['modified_gmt'], true ); + $date_data = $this->server->get_date_with_gmt( $data['modified_gmt'], true ); + if ( ! empty( $date_data ) ) { + list( $post['post_modified'], $post['post_modified_gmt'] ) = $data; + } } // Post slug diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 92cd3707b0..511b483f2a 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -611,10 +611,13 @@ public function parse_date( $date, $force_utc = false ) { * * @param string $date RFC3339 timestamp * @param boolean $force_utc Should we force UTC timestamp? - * @return array Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s) + * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s), null on failure */ public function get_date_with_gmt( $date, $force_utc = false ) { $datetime = $this->parse_date( $date, $force_utc ); + if ( empty( $datetime ) ) { + return null; + } $datetime->setTimezone( self::get_timezone() ); $local = $datetime->format( 'Y-m-d H:i:s' ); From 1a98072d93066b7fc288fdeb3a50363bdd98afae Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 24 May 2014 20:03:29 +1000 Subject: [PATCH 166/515] Remove duplicate parse_date/get_date_with_gmt These were previously marked protected, so removing. --- lib/class-wp-json-posts.php | 43 ------------------------------------- 1 file changed, 43 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 098da83fa5..c0120a02a3 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -958,49 +958,6 @@ protected function insert_post( $data ) { return $post_ID; } - /** - * Parse an RFC3339 timestamp into a DateTime - * - * @param string $date RFC3339 timestamp - * @param boolean $force_utc Force UTC timezone instead of using the timestamp's TZ? - * @return DateTime - */ - protected function parse_date( $date, $force_utc = false ) { - // Default timezone to the server's current one - $timezone = self::get_timezone(); - if ( $force_utc ) { - $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date ); - $timezone = new DateTimeZone( 'UTC' ); - } - - // Strip millisecond precision (a full stop followed by one or more digits) - if ( strpos( $date, '.' ) !== false ) { - $date = preg_replace( '/\.\d+/', '', $date ); - } - $datetime = WP_JSON_DateTime::createFromFormat( DateTime::RFC3339, $date ); - - return $datetime; - } - - /** - * Get a local date with its GMT equivalent, in MySQL datetime format - * - * @param string $date RFC3339 timestamp - * @param boolean $force_utc Should we force UTC timestamp? - * @return array Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s) - */ - protected function get_date_with_gmt( $date, $force_utc = false ) { - $datetime = $this->server->parse_date( $date, $force_utc ); - - $datetime->setTimezone( self::get_timezone() ); - $local = $datetime->format( 'Y-m-d H:i:s' ); - - $datetime->setTimezone( new DateTimeZone( 'UTC' ) ); - $utc = $datetime->format('Y-m-d H:i:s'); - - return array( $local, $utc ); - } - /** * Prepares comment data for returning as a JSON response. * From 7242924a4a0e4cbade1807e4df7da43307fc2d28 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sat, 24 May 2014 12:36:52 -0500 Subject: [PATCH 167/515] Added modified and ping keys to data parameter details and corrected parameter type in post route docs. --- docs/routes/posts.md | 144 +++++++++++++++++++++++++++---------------- 1 file changed, 90 insertions(+), 54 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index 0930acaade..1099aabb4d 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -9,7 +9,7 @@ Create a Post ### Input The `data` parameter consists of the elements of the Post object to be created. This data can be submitted via a regular HTTP multipart body, with -Post values set as values to the `data` parameter, or through a direct JSON +the Post keys and values set to the `data` parameter, or through a direct JSON body. That is, the following are equivalent: @@ -23,35 +23,53 @@ Content-Type: application/json {"title":"Hello World!","content_raw":"Content","excerpt_raw":"Excerpt"} -The `data` parameter should be an array containing the following key value pairs: +The `data` parameter should be an object containing the following key value +pairs: -* `post_title` - Title of the post. (string) *required* -* `content_raw` - Full text of the post. (string) *required* +* `post_title` - Title of the post. (string) __*required*__ +* `content_raw` - Full text of the post. (string) __*required*__ * `excerpt_raw` - Text for excerpt of the post. (string) *optional* * `name` - Slug of the post. (string) *optional* -*`status` - Post status of the post: `draft`, `publish`, `pending`, `future`, -`private`, or any custom registered status. If providing a status of `future`, -you must specify a `date` in order for the post to be published as expected. -Default is `draft`. (string) *optional* +* `status` - Post status of the post: `draft`, `publish`, `pending`, `future`, + `private`, or any custom registered status. If providing a status of + `future`, you must specify a `date` in order for the post to be published as + expected. Default is `draft`. (string) *optional* * `type` - Post type of the post: `post`, `page`, `link`, `nav_menu_item`, or -a any custom registered type. Default is `post`. (string) *optional* -* `date` - Time the post was, or should be, published. Date should be an -RFC3339 timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014-01- -01T12:20:52Z. Default is the local date and time. (string) *optional* -* `date_gmt` - Time the post was, or should be, published in GMT. Date should -be an [RFC3339 timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014 -01-01T12:20:52Z. Default is the current GMT date and time. (string) *optional* + a any custom registered type. Default is `post`. (string) *optional* +* `date` - Date and time the post was, or should be, published in local time. + Date should be an RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). + Example: 2014-01-01T12:20:52Z. Default is the local date and time. (string) + *optional* +* `date_gmt` - Date and time the post was, or should be, published in UTC time. + Date should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). + Example: 201401-01T12:20:52Z. Default is the current GMT date and time. + (string) *optional* +* `modified` - Date and time the post was last modified in local time. Date + should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). + Example: 201401-01T12:20:52Z. Default is the local date and time. (string) + *optional* +* `modified_gmt` - Date and time the post was last modified in UTC time. Date + should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). + Example: 201401-01T12:20:52Z. Default is the current GMT date and time. + (string) *optional* * `author` - Author of the post. Author can be provided as a string of the -author's ID or as the User object of the author. Default is current user. -(object \| string) *optional* + author's ID or as the User object of the author. Default is current user. + (object \| string) *optional* * `password` - Password for protecting the posts. Default is empty string. -(string) *optional* -* `post_parent` - Post ID of the post parent. Default is 0. (integer) *optional* -* `post_format` - Format of the post. Default is `standard`. (string) *optional* + (string) *optional* +* `post_parent` - Post ID of the post parent. Default is 0. (integer) + *optional* +* `post_format` - Format of the post. Default is `standard`. (string) + *optional* * `menu_order` - The order in which posts specified as the `page` type should -appear in supported menus. Default 0. (integer) *optional* -* `comment_status` - Comment status for the post: `open` or`closed`. Default -is the option 'default_comment_status', or 'closed'. (string) *optional* + appear in supported menus. Default 0. (integer) *optional* +* `comment_status` - Comment status for the post: `open` or `closed`. + Indicates whether users can submit comments to the post. Default is the + option 'default_comment_status', or 'closed'. (string) *optional* +* `ping_status` - Ping status for the post: `open` or `closed`. Indicates + whether users can submit pingbacks or trackbacks to the post. Default is the + option 'default_ping_status'. (string) *optional* + ### Response On a successful creation, a 201 Created status is given, indicating that the @@ -70,8 +88,7 @@ posts. ### Input #### `filter` -The `filter` parameter controls the query parameters. It is essentially a subset -of the parameters available to [`WP_Query`](http://codex.wordpress.org/Class_Reference/WP_Query). +The `filter` parameter controls the query parameters. It is essentially a subset of the parameters available to [`WP_Query`](http://codex.wordpress.org/Class_Reference/WP_Query). The parameter should be an array of the following key/value pairs: @@ -81,8 +98,8 @@ The parameter should be an array of the following key/value pairs: * `numberposts` - Number of posts to retrieve, use `-1` for all posts. Default is set by the site. (integer) * `offset` - Number of posts to skip. Default is 0. (integer) -* `orderby` - Parameter to search by, as per [WP Query](http://codex.wordpress.org/Class_Reference/WP_Query#Order_.26_Orderby_Parameters). - Default is "date". (string) +* `orderby` - Parameter to search by, as per [WP Query](http://codex.wordpress.org/Class_Reference/WP_Query#Order_.26_Orderby_Parameters). Default is + "date". (string) * `order` - Order to sort by. Default is "DESC". (string, "ASC" or "DESC") * `s` - Keyword to search for. (string) @@ -119,8 +136,8 @@ Retrieve a Post ### Input #### `context` -The `context` parameter controls the format of the data to return. The following -contexts are available: +The `context` parameter controls the format of the data to return. The +following contexts are available: * `view`: The default context. Gives the normal User entity. * `edit`: Context used for extra fields relevant to updating a user. Includes @@ -148,39 +165,57 @@ recommended to use PUT if available to fit with REST convention. ### Input The `data` parameter consists of Post ID and the elements of the Post object to be modified. This data can be submitted via a regular HTTP multipart body, -with Post values set as values to the `data` parameter, or through a direct +with the Post keys and values set to the `data` parameter, or through a direct JSON body. See the Create Post endpoint for an example. -The `data` parameter should be an array containing the following key value pairs: +The `data` parameter should be an object containing the following key value +pairs: -* `ID` - Unique ID of the post. (integer) *required* -* `post_title` - Title of the post. (string) *optional* -* `content_raw` - Full text of the post. (string) *optional* +* `ID` - Unique ID of the post. (integer) __*required*__ +* `post_title` - Title of the post. (string) __*required*__ +* `content_raw` - Full text of the post. (string) __*required*__ * `excerpt_raw` - Text for excerpt of the post. (string) *optional* * `name` - Slug of the post. (string) *optional* -*`status` - Post status of the post: `draft`, `publish`, `pending`, `future`, -`private`, or any custom registered status. If providing a status of `future`, -you must specify a `date` in order for the post to be published as expected. -Default is `draft`. (string) *optional* +* `status` - Post status of the post: `draft`, `publish`, `pending`, `future`, + `private`, or any custom registered status. If providing a status of + `future`, you must specify a `date` in order for the post to be published as + expected. Default is `draft`. (string) *optional* * `type` - Post type of the post: `post`, `page`, `link`, `nav_menu_item`, or -a any custom registered type. Default is `post`. (string) *optional* -* `date` - Time the post was, or should be, published. Date should be an -RFC3339 timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014-01- -01T12:20:52Z. Default is the local date and time. (string) *optional* -* `date_gmt` - Time the post was, or should be, published in GMT. Date should -be an [RFC3339 timestamp](http://www.ietf.org/rfc/rfc3339.txt). Example: 2014 -01-01T12:20:52Z. Default is the current GMT date and time. (string) *optional* + a any custom registered type. Default is `post`. (string) *optional* +* `date` - Date and time the post was, or should be, published in local time. + Date should be an RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). + Example: 2014-01-01T12:20:52Z. Default is the local date and time. (string) + *optional* +* `date_gmt` - Date and time the post was, or should be, published in UTC time. + Date should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). + Example: 201401-01T12:20:52Z. Default is the current GMT date and time. + (string) *optional* +* `modified` - Date and time the post was last modified in local time. Date + should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). + Example: 201401-01T12:20:52Z. Default is the local date and time. (string) + *optional* +* `modified_gmt` - Date and time the post was last modified in UTC time. Date + should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). + Example: 201401-01T12:20:52Z. Default is the current GMT date and time. + (string) *optional* * `author` - Author of the post. Author can be provided as a string of the -author's ID or as the User object of the author. Default is current user. -(object \| string) *optional* + author's ID or as the User object of the author. Default is current user. + (object \| string) *optional* * `password` - Password for protecting the posts. Default is empty string. -(string) *optional* -* `post_parent` - Post ID of the post parent. Default is 0. (integer) *optional* -* `post_format` - Format of the post. Default is `standard`. (string) *optional* + (string) *optional* +* `post_parent` - Post ID of the post parent. Default is 0. (integer) + *optional* +* `post_format` - Format of the post. Default is `standard`. (string) + *optional* * `menu_order` - The order in which posts specified as the `page` type should -appear in supported menus. Default 0. (integer) *optional* -* `comment_status` - Comment status for the post: `open` or`closed`. Default -is the option 'default_comment_status', or 'closed'. (string) *optional* + appear in supported menus. Default 0. (integer) *optional* +* `comment_status` - Comment status for the post: `open` or `closed`. + Indicates whether users can submit comments to the post. Default is the + option 'default_comment_status', or 'closed'. (string) *optional* +* `ping_status` - Ping status for the post: `open` or `closed`. Indicates + whether users can submit pingbacks or trackbacks to the post. Default is the + option 'default_ping_status'. (string) *optional* + ### Response On a successful update, a 200 OK status is given, indicating the post has been @@ -220,7 +255,8 @@ The supplied data should be a Meta object. This data can be submitted via a regular HTTP multipart body, with the Meta key and value set with the `data` parameter, or through a direct JSON body. -The `data` parameter should be an array containing the following key value pairs: +The `data` parameter should be an object containing the following key value +pairs: * `key` - The post meta key to be created. (string) *required* * `value` - The post meta value for the key provided. (string) *required* From fc30b0041aa9c13da831b982f254d29838b584a7 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sat, 24 May 2014 13:39:25 -0500 Subject: [PATCH 168/515] Set post dates and modified dates with the correct variable. Fixes #219. --- lib/class-wp-json-posts.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 857fa57131..941c79693e 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1185,13 +1185,13 @@ protected function insert_post( $data ) { if ( ! empty( $data['date'] ) ) { $date_data = $this->server->get_date_with_gmt( $data['date'] ); if ( ! empty( $date_data ) ) { - list( $post['post_date'], $post['post_date_gmt'] ) = $data; + list( $post['post_date'], $post['post_date_gmt'] ) = $date_data; } } elseif ( ! empty( $data['date_gmt'] ) ) { $date_data = $this->server->get_date_with_gmt( $data['date_gmt'], true ); if ( ! empty( $date_data ) ) { - list( $post['post_date'], $post['post_date_gmt'] ) = $data; + list( $post['post_date'], $post['post_date_gmt'] ) = $date_data; } } @@ -1199,13 +1199,13 @@ protected function insert_post( $data ) { if ( ! empty( $data['modified'] ) ) { $date_data = $this->server->get_date_with_gmt( $data['modified'] ); if ( ! empty( $date_data ) ) { - list( $post['post_modified'], $post['post_modified_gmt'] ) = $data; + list( $post['post_modified'], $post['post_modified_gmt'] ) = $date_data; } } elseif ( ! empty( $data['modified_gmt'] ) ) { $date_data = $this->server->get_date_with_gmt( $data['modified_gmt'], true ); if ( ! empty( $date_data ) ) { - list( $post['post_modified'], $post['post_modified_gmt'] ) = $data; + list( $post['post_modified'], $post['post_modified_gmt'] ) = $date_data; } } From 95eff06b9327f512000a18d62b4da3f17fdfeb2c Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 25 May 2014 13:21:59 +1000 Subject: [PATCH 169/515] Bump to 1.0 --- CHANGELOG.md | 284 +++++++++++++++++++++++++++++++++ lib/class-wp-json-datetime.php | 1 - lib/class-wp-json-server.php | 1 - plugin.php | 4 +- 4 files changed, 286 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c59547b64..75023ac658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,289 @@ # Changelog +## 1.0 + +- Add user endpoints. + + Creating, reading, updating and deleting users and their data is now possible + by using the `/users` endpoints. `/users/me` can be used to determine the + current user, and returns a 401 status for non-logged in users. + + Note that the format of post authors has changed, as it is now an embedded + User entity. This should not break backwards compatibility. + + Custom post types gain this ability automatically. + + (props @tobych, @rmccue, [#20][gh-20], [#146][gh-146]) + +- Add post meta endpoints. + + Creating, reading, updating and deleting post meta is now possible by using + the `/posts//meta` endpoints. Post meta is now correctly embedded into + Post entities. + + Meta can be updated via the Post entity (e.g. `PUT` to `/posts/`) or via + the entity itself at `/posts//meta/`. Meta deletion must be done via + a `DELETE` request to the latter. + + Only non-protected and non-serialized meta can be accessed or manipulated via + the API. This is not predicted to change in the future; clients wishing to + access this data should consider alternative approaches. + + Custom post types do not currently gain this ability automatically. + + (props @attitude, @alisspers, @rachelbaker, @rmccue, @tlovett1, @tobych, + @zedejose, [#68][gh-68], [#168][gh-168], [#189][gh-189], [#207][gh-207]) + +- Add endpoint for deleting a single comment. + + Clients can now send a `DELETE` request to comment routes to delete + the comment. + + Custom post types supporting comments will gain this ability automatically. + + (props @tlovett1, @rmccue, [#178][gh-178], [#191][gh-191]) + +- Add endpoint for post revisions. + + Post revisions are now available at `/posts//revisions`, and are linked in + the `meta.links.version-history` key of post entities. + + Custom post types supporting revisions will gain this ability automatically. + + (props @tlovett1, [#193][gh-193]) + +- Respond to requests without depending on pretty permalink settings. + + For sites without pretty permalinks enabled, the API is now available from + `?json_route=/`. Clients should check for this via the autodiscovery methods + (Link header or RSD). + + (props @rmccue, [#69][gh-69], [#138][gh-138]) + +- Add register post type argument. + + Post types can now indicate their availability via the API using the + `show_in_json` argument passed to `register_post_type`. This value defaults to + the `publicly_queryable` argument (which itself defaults to the + `public` argument). + + (props @iandunn, @rmccue, [#145][gh-145]) + +- Remove basic authentication handler. + + **This breaks backwards compatibility** for clients using Basic + authentication. Clients are encouraged to switch to using [OAuth + authentication][OAuth1]. The [Basic Authentication plugin][Basic-Auth] can be + installed for backwards compatibility and local development, however should + not be used in production. + + (props @rmccue, [#37][gh-37], [#152][gh-152]) + +- Require nonces for cookie-based authentication. + + **This breaks backwards compatibility** and requires any clients using cookie + authentication to also send a nonce with the request. The built-in Javascript + API automatically handles this. + + (props @rmccue, [#177][gh-177], [#180][gh-180]) + +- Clean up deprecated methods/functions. + + Functions and methods previously deprecated in 0.8/0.9 have now been removed. + Future deprecations will take place in the same manner as WordPress core. + + **This breaks backwards compatibility**, however these were marked as + deprecated in previous releases. + + (props @rmccue, [#187][gh-187]) + +- Only expose meta on 'edit' context as a temporary workaround. + + Privacy concerns around exposing meta to all users necessitate this change. + + **This breaks backwards compatibility** as post meta data is no longer + available to all users. Clients wishing to access this data should + authenticate and use the `edit` context. + + (props @iandunn, @rmccue, [#135][gh-135]) + +- Add `json_ensure_response` function to ensure either a + `WP_JSON_ResponseInterface` or a `WP_Error` object is returned. + + When extending the API, the `json_ensure_response` function can be used to + ensure that any raw data returned is wrapped with a `WP_JSON_Response` object. + This allows using `get_status`/`get_data` easily, however `WP_Error` must + still be checked via `is_wp_error`. + + (props @rmccue, [#151][gh-151], [#154][gh-154]) + +- Use version option to check on init if rewrite rules should be flushed. + + Rewrite rules on multisite are now flushed via an init hook, rather than + switching to each site on activation. + + (props @rachelbaker, [#149][gh-149]) + +- Fix typo in schema docs + + (props @codebykat, [#132][gh-132]) + +- Add check for valid JSON data before using to avoid parameter overwrite. + + When passing data to an endpoint that accepts JSON data, the data will now be + validated before passing to the endpoint. + + (props @rachelbaker, @rmccue, [#133][gh-133]) + +- Add authentication property to site index. + + (props @rmccue, [#131][gh-131]) + +- Move the test helper to a subdirectory. + + The plugin will now no longer prompt for updates due to the helper. + + (props @rmccue, [#127][gh-127]) + +- Include post ID with `json_prepare_meta` filter. + + (props @rmccue, [#137][gh-137]) + +- Corrected parameter names in x-form examples in docs. + + (props @rachelbaker, [#134][gh-134]) + +- Pass `WP_JSON_Server` instance to `json_serve_request`. + + (props @alisspers, @rmccue, [#61][gh-61], [#139][gh-139]) + +- Don't use deprecated function in `WP_JSON_Posts::edit_post()` + + (props @rachelbaker, [#150][gh-150]) + +- Pass post ID to `json_insert_post` action during both insert and update. + + (props @cmmarslender, [#148][gh-148]) + +- Add descriptions to taxonomy term data. + + (props @pushred, [#111][gh-111]) + +- Ensure we handle raw data passed to the API. + + (props @tlovett1, @rmccue, [#91][gh-91], [#155][gh-155]) + +- Remove unused `prepare_author` method from `WP_JSON_Posts` class. + + (props @rachelbaker, [#165][gh-165]) + +- Add multiple post type support to get_posts method. + + (props @rmccue, [#142][gh-142], [#163][gh-163]) + +- Return `WP_Error` in `WP_JSON_Posts::get_comment` for invalid comments. + + (props @tlovett1, [#166][gh-166], [#171][gh-171]) + +- Update getting started documentation. + + (props @rmccue, [#176][gh-176]) + +- Improve and clarify "array" input syntax documentation. + + (props @rmccue, [#140][gh-140], [#175][gh-175]) + +- Update post routes documentation. + + (props @rmccue, [#172][gh-172], [#174][gh-174]) + +- Add documentation for user endpoints. + + (props @rachelbaker, @rmccue, [#158][gh-158]) + +- Add permalink settings step to Quick Setup instructions. + + (props @kadamwhite, [#183][gh-183]) + +- Update taxonomy collection to return indexed array. + + (props @mattheu, [#184][gh-184]) + +- Remove placeholder endpoints. + + (props @rmccue, [#161][gh-161], [#192][gh-192]) + +- Fix issues with embedded attachments. + + Checks that the post supports attachment data before adding it, and ensures we + don't embed entities many layers deep. + + (props @rmccue, [#194][gh-194]) + +- Change post parent preparation context to embed. + + (props @rmccue, [#195][gh-195]) + +- Change server meta links to reference the WP-API organization GitHub repo. + + (props @rachelbaker, [#208][gh-208]) + +- Fix plugin tests + + (props @rmccue, [#215][gh-215]) + +- Check for errors with invalid dates and remove duplicate date parsing + methods. + + (props @rachelbaker, @rmccue, [#216][gh-216], [#219][gh-219]) + +[View all changes](https://github.com/rmccue/WP-API/compare/0.9...1.0) + +[OAuth1]: https://github.com/WP-API/OAuth1 +[Basic-Auth]: https://github.com/WP-API/Basic-Auth +[gh-111]: https://github.com/WP-API/WP-API/issues/111 +[gh-127]: https://github.com/WP-API/WP-API/issues/127 +[gh-131]: https://github.com/WP-API/WP-API/issues/131 +[gh-132]: https://github.com/WP-API/WP-API/issues/132 +[gh-133]: https://github.com/WP-API/WP-API/issues/133 +[gh-134]: https://github.com/WP-API/WP-API/issues/134 +[gh-135]: https://github.com/WP-API/WP-API/issues/135 +[gh-137]: https://github.com/WP-API/WP-API/issues/137 +[gh-138]: https://github.com/WP-API/WP-API/issues/138 +[gh-139]: https://github.com/WP-API/WP-API/issues/139 +[gh-145]: https://github.com/WP-API/WP-API/issues/145 +[gh-146]: https://github.com/WP-API/WP-API/issues/146 +[gh-148]: https://github.com/WP-API/WP-API/issues/148 +[gh-149]: https://github.com/WP-API/WP-API/issues/149 +[gh-150]: https://github.com/WP-API/WP-API/issues/150 +[gh-152]: https://github.com/WP-API/WP-API/issues/152 +[gh-154]: https://github.com/WP-API/WP-API/issues/154 +[gh-155]: https://github.com/WP-API/WP-API/issues/155 +[gh-158]: https://github.com/WP-API/WP-API/issues/158 +[gh-161]: https://github.com/WP-API/WP-API/issues/161 +[gh-163]: https://github.com/WP-API/WP-API/issues/163 +[gh-165]: https://github.com/WP-API/WP-API/issues/165 +[gh-171]: https://github.com/WP-API/WP-API/issues/171 +[gh-174]: https://github.com/WP-API/WP-API/issues/174 +[gh-175]: https://github.com/WP-API/WP-API/issues/175 +[gh-176]: https://github.com/WP-API/WP-API/issues/176 +[gh-177]: https://github.com/WP-API/WP-API/issues/177 +[gh-178]: https://github.com/WP-API/WP-API/issues/178 +[gh-180]: https://github.com/WP-API/WP-API/issues/180 +[gh-183]: https://github.com/WP-API/WP-API/issues/183 +[gh-184]: https://github.com/WP-API/WP-API/issues/184 +[gh-187]: https://github.com/WP-API/WP-API/issues/187 +[gh-191]: https://github.com/WP-API/WP-API/issues/191 +[gh-192]: https://github.com/WP-API/WP-API/issues/192 +[gh-193]: https://github.com/WP-API/WP-API/issues/193 +[gh-194]: https://github.com/WP-API/WP-API/issues/194 +[gh-195]: https://github.com/WP-API/WP-API/issues/195 +[gh-207]: https://github.com/WP-API/WP-API/issues/207 +[gh-208]: https://github.com/WP-API/WP-API/issues/208 +[gh-215]: https://github.com/WP-API/WP-API/issues/215 +[gh-216]: https://github.com/WP-API/WP-API/issues/216 +[gh-219]: https://github.com/WP-API/WP-API/issues/219 + ## 0.9 - Move from `wp-json.php/` to `wp-json/` diff --git a/lib/class-wp-json-datetime.php b/lib/class-wp-json-datetime.php index d2e93c15cc..ae04419b72 100644 --- a/lib/class-wp-json-datetime.php +++ b/lib/class-wp-json-datetime.php @@ -4,7 +4,6 @@ * * @package WordPress * @subpackage JSON API - * @version 0.9 */ class WP_JSON_DateTime extends DateTime { /** diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 511b483f2a..540e636d0d 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -5,7 +5,6 @@ * Contains the WP_JSON_Server class. * * @package WordPress - * @version 0.9 */ require_once ABSPATH . 'wp-admin/includes/admin.php'; diff --git a/plugin.php b/plugin.php index 5f85de9ef1..3571b043fc 100644 --- a/plugin.php +++ b/plugin.php @@ -4,7 +4,7 @@ * Description: JSON-based REST API for WordPress, developed as part of GSoC 2013. * Author: Ryan McCue * Author URI: http://ryanmccue.info/ - * Version: 0.9 + * Version: 1.0 * Plugin URI: https://github.com/rmccue/WP-API */ @@ -13,7 +13,7 @@ * * @var string */ -define('JSON_API_VERSION', '0.9'); +define('JSON_API_VERSION', '1.0'); /** * Include our files for the API From bf25ed933e85ec8ebd18a2c8d44802722b780286 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 25 May 2014 13:25:11 +1000 Subject: [PATCH 170/515] Add missing issue links --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75023ac658..ea861394e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -241,6 +241,12 @@ [OAuth1]: https://github.com/WP-API/OAuth1 [Basic-Auth]: https://github.com/WP-API/Basic-Auth +[gh-20]: https://github.com/WP-API/WP-API/issues/20 +[gh-37]: https://github.com/WP-API/WP-API/issues/37 +[gh-61]: https://github.com/WP-API/WP-API/issues/61 +[gh-68]: https://github.com/WP-API/WP-API/issues/68 +[gh-69]: https://github.com/WP-API/WP-API/issues/69 +[gh-91]: https://github.com/WP-API/WP-API/issues/91 [gh-111]: https://github.com/WP-API/WP-API/issues/111 [gh-127]: https://github.com/WP-API/WP-API/issues/127 [gh-131]: https://github.com/WP-API/WP-API/issues/131 @@ -251,11 +257,14 @@ [gh-137]: https://github.com/WP-API/WP-API/issues/137 [gh-138]: https://github.com/WP-API/WP-API/issues/138 [gh-139]: https://github.com/WP-API/WP-API/issues/139 +[gh-140]: https://github.com/WP-API/WP-API/issues/140 +[gh-142]: https://github.com/WP-API/WP-API/issues/142 [gh-145]: https://github.com/WP-API/WP-API/issues/145 [gh-146]: https://github.com/WP-API/WP-API/issues/146 [gh-148]: https://github.com/WP-API/WP-API/issues/148 [gh-149]: https://github.com/WP-API/WP-API/issues/149 [gh-150]: https://github.com/WP-API/WP-API/issues/150 +[gh-151]: https://github.com/WP-API/WP-API/issues/151 [gh-152]: https://github.com/WP-API/WP-API/issues/152 [gh-154]: https://github.com/WP-API/WP-API/issues/154 [gh-155]: https://github.com/WP-API/WP-API/issues/155 @@ -263,7 +272,10 @@ [gh-161]: https://github.com/WP-API/WP-API/issues/161 [gh-163]: https://github.com/WP-API/WP-API/issues/163 [gh-165]: https://github.com/WP-API/WP-API/issues/165 +[gh-166]: https://github.com/WP-API/WP-API/issues/166 +[gh-168]: https://github.com/WP-API/WP-API/issues/168 [gh-171]: https://github.com/WP-API/WP-API/issues/171 +[gh-172]: https://github.com/WP-API/WP-API/issues/172 [gh-174]: https://github.com/WP-API/WP-API/issues/174 [gh-175]: https://github.com/WP-API/WP-API/issues/175 [gh-176]: https://github.com/WP-API/WP-API/issues/176 @@ -273,6 +285,7 @@ [gh-183]: https://github.com/WP-API/WP-API/issues/183 [gh-184]: https://github.com/WP-API/WP-API/issues/184 [gh-187]: https://github.com/WP-API/WP-API/issues/187 +[gh-189]: https://github.com/WP-API/WP-API/issues/189 [gh-191]: https://github.com/WP-API/WP-API/issues/191 [gh-192]: https://github.com/WP-API/WP-API/issues/192 [gh-193]: https://github.com/WP-API/WP-API/issues/193 From 7da000ac6bf1ee219d0cc4799e384aaeda1dba88 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 25 May 2014 13:01:57 -0500 Subject: [PATCH 171/515] Added basic create and edit post tests. --- tests/test_json_posts.php | 74 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_json_posts.php diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php new file mode 100644 index 0000000000..ea13fadcc9 --- /dev/null +++ b/tests/test_json_posts.php @@ -0,0 +1,74 @@ +author_id = $this->factory->user->create( array( 'role' => 'editor' ) ); + wp_set_current_user( $this->author_id ); + + $this->fake_server = $this->getMock('WP_JSON_Server'); + $this->endpoint = new WP_JSON_Posts( $this->fake_server ); + } + + function _set_data( $args = array() ) { + $defaults = array( + 'title' => rand_str(), + 'content_raw' => rand_str(), + 'excerpt_raw' => rand_str(), + 'name' => 'test', + 'status' => 'publish', + 'author' => $this->author_id, + ); + + return wp_parse_args( $args, $defaults ); + } + + function test_create_post() { + $input = $this->_set_data(); + + $response = $this->endpoint->new_post( $input ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 201, $response->get_status() ); + + $output = $response->get_data(); + $post = get_post( $output['ID'] ); + + $this->assertEquals( $input['title'], $post->post_title ); + $this->assertEquals( $input['content_raw'], $post->post_content ); + $this->assertEquals( $input['excerpt_raw'], $post->post_excerpt ); + $this->assertEquals( $input['name'], $post->post_name ); + $this->assertEquals( $input['status'], $post->post_status ); + $this->assertEquals( $input['author'], $post->post_author ); + } + + function test_edit_post() { + $post_id = $this->factory->post->create(); + $input = $this->_set_data( array( 'ID' => $post_id ) ) ; + + $response = $this->endpoint->edit_post( $post_id, $input ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $output = $response->get_data(); + $post = get_post( $post_id ); + + $this->assertEquals( $input['title'], $post->post_title ); + $this->assertEquals( $input['content_raw'], $post->post_content ); + $this->assertEquals( $input['excerpt_raw'], $post->post_excerpt ); + $this->assertEquals( $input['name'], $post->post_name ); + $this->assertEquals( $input['status'], $post->post_status ); + $this->assertEquals( $input['author'], $post->post_author ); + } + +} From 9264a730a86951cc4c7edcde53077d6bd3d0bff1 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 26 May 2014 12:16:55 -0500 Subject: [PATCH 172/515] Added header tests to create and new posts. --- tests/test_json_posts.php | 61 ++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index ea13fadcc9..835aa6fdbe 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -10,6 +10,9 @@ class WP_Test_JSON_Posts extends WP_UnitTestCase { public function setUp() { parent::setUp(); + $this->post_id = $this->factory->post->create(); + $this->post_obj = get_post( $this->post_id ); + $this->author_id = $this->factory->user->create( array( 'role' => 'editor' ) ); wp_set_current_user( $this->author_id ); @@ -17,7 +20,7 @@ public function setUp() { $this->endpoint = new WP_JSON_Posts( $this->fake_server ); } - function _set_data( $args = array() ) { + protected function set_data( $args = array() ) { $defaults = array( 'title' => rand_str(), 'content_raw' => rand_str(), @@ -31,44 +34,54 @@ function _set_data( $args = array() ) { } function test_create_post() { - $input = $this->_set_data(); + $data = $this->set_data(); + $response = $this->endpoint->new_post( $data ); - $response = $this->endpoint->new_post( $input ); $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); + $headers = $response->get_headers(); + // Check that we succeeded $this->assertEquals( 201, $response->get_status() ); - - $output = $response->get_data(); - $post = get_post( $output['ID'] ); - - $this->assertEquals( $input['title'], $post->post_title ); - $this->assertEquals( $input['content_raw'], $post->post_content ); - $this->assertEquals( $input['excerpt_raw'], $post->post_excerpt ); - $this->assertEquals( $input['name'], $post->post_name ); - $this->assertEquals( $input['status'], $post->post_status ); - $this->assertEquals( $input['author'], $post->post_author ); + $this->assertArrayHasKey( 'Location', $headers ); + $this->assertArrayHasKey( 'Last-Modified', $headers ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + + $this->assertEquals( $data['title'], $new_post->post_title ); + $this->assertEquals( $data['content_raw'], $new_post->post_content ); + $this->assertEquals( $data['excerpt_raw'], $new_post->post_excerpt ); + $this->assertEquals( $data['name'], $new_post->post_name ); + $this->assertEquals( $data['status'], $new_post->post_status ); + $this->assertEquals( $data['author'], $new_post->post_author ); } function test_edit_post() { - $post_id = $this->factory->post->create(); - $input = $this->_set_data( array( 'ID' => $post_id ) ) ; + $data = $this->set_data( array( 'ID' => $this->post_id ) ) ; + $response = $this->endpoint->edit_post( $this->post_id, $data ); - $response = $this->endpoint->edit_post( $post_id, $input ); $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); + $headers = $response->get_headers(); + // Check that we succeeded $this->assertEquals( 200, $response->get_status() ); + $this->assertArrayHasKey( 'Last-Modified', $headers ); - $output = $response->get_data(); - $post = get_post( $post_id ); + $edited_post = get_post( $this->post_id ); - $this->assertEquals( $input['title'], $post->post_title ); - $this->assertEquals( $input['content_raw'], $post->post_content ); - $this->assertEquals( $input['excerpt_raw'], $post->post_excerpt ); - $this->assertEquals( $input['name'], $post->post_name ); - $this->assertEquals( $input['status'], $post->post_status ); - $this->assertEquals( $input['author'], $post->post_author ); + // Check that the data has been updated + $this->assertEquals( $data['title'], $edited_post->post_title ); + $this->assertEquals( $data['content_raw'], $edited_post->post_content ); + $this->assertEquals( $data['excerpt_raw'], $edited_post->post_excerpt ); + $this->assertEquals( $data['name'], $edited_post->post_name ); + $this->assertEquals( $data['status'], $edited_post->post_status ); + $this->assertEquals( $data['author'], $edited_post->post_author ); } + + + + } From 389de681cb45bb7317256e05fd42a7106cbc8e49 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 26 May 2014 13:03:02 -0500 Subject: [PATCH 173/515] Added test for returning a single post. --- tests/test_json_posts.php | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 835aa6fdbe..e34eb0e1e5 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -33,6 +33,37 @@ protected function set_data( $args = array() ) { return wp_parse_args( $args, $defaults ); } + protected function check_get_post_response( $response, $post_obj, $context = 'view' ) { + $response_data = $response->get_data(); + + $this->assertEquals( $response_data['ID'], $post_obj->ID ); + $this->assertEquals( $response_data['slug'], $post_obj->post_name ); + $this->assertEquals( $response_data['status'], $post_obj->post_status ); + $this->assertEquals( $response_data['author'], $post_obj->post_author ); + $this->assertEquals( $response_data['parent'], $post_obj->post_parent ); + $this->assertEquals( $response_data['link'], get_permalink( $post_obj->ID ) ); + $this->assertEquals( $response_data['menu_order'], $post_obj->menu_order ); + $this->assertEquals( $response_data['comment_status'], $post_obj->comment_status ); + $this->assertEquals( $response_data['ping_status'], $post_obj->ping_status ); + $this->assertEquals( $response_data['sticky'], is_sticky( $post_obj->ID ) ); + + // Check filtered values. + $this->assertEquals( $response_data['title'], get_the_title( $post_obj->ID ) ); + // TODO: apply content filter for more accurate testing. + $this->assertEquals( $response_data['content'], wpautop( $post_obj->post_content ) ); + // TODO: apply excerpt filter for more accurate testing. + $this->assertEquals( $response_data['excerpt'], wpautop( $post_obj->post_excerpt ) ); + $this->assertEquals( $response_data['guid'], $post_obj->guid ); + + // Check raw values when applicable. + if ( $context == 'view-revision' || $context == 'edit' ) { + $this->assertEquals( $response_data['title_raw'], $post_obj->post_title ); + $this->assertEquals( $response_data['content_raw'], $post_obj->post_content ); + $this->assertEquals( $response_data['guid_raw'], $post_obj->guid ); + // TODO: add test for post_meta values. + } + } + function test_create_post() { $data = $this->set_data(); $response = $this->endpoint->new_post( $data ); @@ -57,6 +88,20 @@ function test_create_post() { $this->assertEquals( $data['author'], $new_post->post_author ); } + function test_get_post() { + $response = $this->endpoint->get_post( $this->post_id ); + + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + $headers = $response->get_headers(); + + // Check that we succeeded + $this->assertEquals( 200, $response->get_status() ); + $this->assertArrayHasKey( 'Last-Modified', $headers ); + + $this->check_get_post_response( $response, $this->post_obj ); + } + function test_edit_post() { $data = $this->set_data( array( 'ID' => $this->post_id ) ) ; $response = $this->endpoint->edit_post( $this->post_id, $data ); From 3dc1f3da7377e5a5cf55ad0e30abeb309232a80e Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 26 May 2014 13:03:26 -0500 Subject: [PATCH 174/515] Added response test for edit context of edit_post endpoint. --- tests/test_json_posts.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index e34eb0e1e5..04347ac7bb 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -123,6 +123,8 @@ function test_edit_post() { $this->assertEquals( $data['name'], $edited_post->post_name ); $this->assertEquals( $data['status'], $edited_post->post_status ); $this->assertEquals( $data['author'], $edited_post->post_author ); + + $this->check_get_post_response( $response, $this->post_obj, 'edit' ); } From b3f1d321faefe1ae5f7e69058fa5a6a1a4e7bca2 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 26 May 2014 16:12:09 -0500 Subject: [PATCH 175/515] Added format, password, date and modified date tests to check_get_post_response. --- tests/test_json_posts.php | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 04347ac7bb..429c29bdd8 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -45,8 +45,29 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi $this->assertEquals( $response_data['menu_order'], $post_obj->menu_order ); $this->assertEquals( $response_data['comment_status'], $post_obj->comment_status ); $this->assertEquals( $response_data['ping_status'], $post_obj->ping_status ); + $this->assertEquals( $response_data['password'], $post_obj->post_password ); $this->assertEquals( $response_data['sticky'], is_sticky( $post_obj->ID ) ); + // Check post format. + $post_format = get_post_format( $post_obj->ID ); + if ( empty( $post_format ) ) { + $this->assertEquals( $response_data['format'], 'standard' ); + } else { + $this->assertEquals( $response_data['format'], get_post_format( $post_obj->ID ) ); + } + + // Check post dates. + $timezone = $this->fake_server->get_timezone(); + $post_date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_date, $timezone ); + $post_date_gmt = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_date_gmt ); + $post_modified = $post_date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified, $timezone ); + $post_modified_gmt = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified_gmt ); + $this->assertEquals( $response_data['date'], $post_date->format( 'c' ) ); + $this->assertEquals( $response_data['date_gmt'], $post_date_gmt->format( 'c' ) ); + $this->assertEquals( $response_data['modified'], $post_modified->format( 'c' ) ); + $this->assertEquals( $response_data['modified_gmt'], $post_modified_gmt->format( 'c' ) ); + + // Check filtered values. $this->assertEquals( $response_data['title'], get_the_title( $post_obj->ID ) ); // TODO: apply content filter for more accurate testing. @@ -55,13 +76,6 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi $this->assertEquals( $response_data['excerpt'], wpautop( $post_obj->post_excerpt ) ); $this->assertEquals( $response_data['guid'], $post_obj->guid ); - // Check raw values when applicable. - if ( $context == 'view-revision' || $context == 'edit' ) { - $this->assertEquals( $response_data['title_raw'], $post_obj->post_title ); - $this->assertEquals( $response_data['content_raw'], $post_obj->post_content ); - $this->assertEquals( $response_data['guid_raw'], $post_obj->guid ); - // TODO: add test for post_meta values. - } } function test_create_post() { @@ -86,6 +100,8 @@ function test_create_post() { $this->assertEquals( $data['name'], $new_post->post_name ); $this->assertEquals( $data['status'], $new_post->post_status ); $this->assertEquals( $data['author'], $new_post->post_author ); + + $this->check_get_post_response( $response, $new_post ); } function test_get_post() { @@ -124,7 +140,7 @@ function test_edit_post() { $this->assertEquals( $data['status'], $edited_post->post_status ); $this->assertEquals( $data['author'], $edited_post->post_author ); - $this->check_get_post_response( $response, $this->post_obj, 'edit' ); + $this->check_get_post_response( $response, $edited_post, 'edit' ); } From c3fc432d3f2111a940c3bb11462605d548524837 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 26 May 2014 21:06:44 -0500 Subject: [PATCH 176/515] Created api comparison doc with initial pass of auth, users, and posts comparison info. --- docs/comparison.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 docs/comparison.md diff --git a/docs/comparison.md b/docs/comparison.md new file mode 100644 index 0000000000..0ca341f4ff --- /dev/null +++ b/docs/comparison.md @@ -0,0 +1,65 @@ +# WordPress JSON API Comparison + +The purpose of this document is to compare the WordPress JSON REST API (WP +API) to the other WordPress JSON API projects. Included in this comparison +are the (WordPress.com JSON REST API)[http://developer.wordpress.com/docs/api/], +and... + +## Authentication + +| Feature | API | Supported | +|:-------------- |:----------------|:----------:| +| Cookie-based | WP API | X | +| Cookie-based | WP.com JSON API | | +| OAuth1 | WP API | X | +| OAuth1 | WP.com JSON API | | +| OAuth2 | WP API | | +| OAuth2 | WP.com JSON API | X | + +## Site + +| Feature | API | Supported | +|:-----------------------|:----------------|:----------:| +| Basic Site Information | WP API | X | +| Basic Site Information | WP.com JSON API | X | + + +## Users + +| Feature | API | Supported | +|:----------------------|:----------------|:----------:| +| Create a user | WP API | X | +| Create a user | WP.com JSON API | | +| List all users | WP API | X | +| List all users | WP.com JSON API | X | +| Retrieve a user | WP API | X | +| Retrieve a user | WP.com JSON API | | +| Retrieve current user | WP API | X | +| Retrieve current user | WP.com JSON API | X | +| Edit a user | WP API | X | +| Edit a user | WP.com JSON API | | +| Delete a user | WP API | X | +| Delete a user | WP.com JSON API | | + +## Posts + +| Feature | API | Supported | +|:-------------------------|:----------------|:----------:| +| Create a post | WP API | X | +| Create a post | WP.com JSON API | X | +| Create meta for a post | WP API | X | +| Create meta for a post | WP.com JSON API | X | +| List all posts | WP API | X | +| List all posts | WP.com JSON API | X | +| Retrieve a post | WP API | X | +| Retrieve a post | WP.com JSON API | X | +| Retrieve meta for a post | WP API | X | +| Retrieve meta for a post | WP.com JSON API | X | +| Edit a post | WP API | X | +| Edit a post | WP.com JSON API | X | +| Edit meta for a post | WP API | X | +| Edit meta for a post | WP.com JSON API | X | +| Delete a post | WP API | X | +| Delete a post | WP.com JSON API | X | +| Delete meta for a post | WP API | X | +| Delete meta for a post | WP.com JSON API | ? | From 62177190a9a203a79fca2498fdbb9542b7c2ab12 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 13:11:22 +1000 Subject: [PATCH 177/515] Reformat to column comparison --- docs/comparison.md | 73 +++++++++++++++++----------------------------- 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/docs/comparison.md b/docs/comparison.md index 0ca341f4ff..d2bc690af2 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -7,59 +7,40 @@ and... ## Authentication -| Feature | API | Supported | -|:-------------- |:----------------|:----------:| -| Cookie-based | WP API | X | -| Cookie-based | WP.com JSON API | | -| OAuth1 | WP API | X | -| OAuth1 | WP.com JSON API | | -| OAuth2 | WP API | | -| OAuth2 | WP.com JSON API | X | +| Feature | WP API | WP.com JSON API | +|:-------------------------|:-------|:----------------:| +| Cookie-based | X | | +| OAuth1 | X | | +| OAuth2 | | X | ## Site -| Feature | API | Supported | -|:-----------------------|:----------------|:----------:| -| Basic Site Information | WP API | X | -| Basic Site Information | WP.com JSON API | X | +| Feature | WP API | WP.com JSON API | +|:-------------------------|:-------|:---------------:| +| Basic Site Information | X | X | ## Users -| Feature | API | Supported | -|:----------------------|:----------------|:----------:| -| Create a user | WP API | X | -| Create a user | WP.com JSON API | | -| List all users | WP API | X | -| List all users | WP.com JSON API | X | -| Retrieve a user | WP API | X | -| Retrieve a user | WP.com JSON API | | -| Retrieve current user | WP API | X | -| Retrieve current user | WP.com JSON API | X | -| Edit a user | WP API | X | -| Edit a user | WP.com JSON API | | -| Delete a user | WP API | X | -| Delete a user | WP.com JSON API | | +| Feature | WP API | WP.com JSON API | +|:-------------------------|:--------|:---------------:| +| Create a user | X | | +| List all users | X | X | +| Retrieve a user | X | | +| Retrieve current user | X | X | +| Edit a user | X | | +| Delete a user | X | | ## Posts -| Feature | API | Supported | -|:-------------------------|:----------------|:----------:| -| Create a post | WP API | X | -| Create a post | WP.com JSON API | X | -| Create meta for a post | WP API | X | -| Create meta for a post | WP.com JSON API | X | -| List all posts | WP API | X | -| List all posts | WP.com JSON API | X | -| Retrieve a post | WP API | X | -| Retrieve a post | WP.com JSON API | X | -| Retrieve meta for a post | WP API | X | -| Retrieve meta for a post | WP.com JSON API | X | -| Edit a post | WP API | X | -| Edit a post | WP.com JSON API | X | -| Edit meta for a post | WP API | X | -| Edit meta for a post | WP.com JSON API | X | -| Delete a post | WP API | X | -| Delete a post | WP.com JSON API | X | -| Delete meta for a post | WP API | X | -| Delete meta for a post | WP.com JSON API | ? | +| Feature | WP API | WP.com JSON API | +|:-------------------------|:-------|:---------------:| +| Create a post | X | X | +| Create meta for a post | X | X | +| List all posts | X | X | +| Retrieve a post | X | X | +| Retrieve meta for a post | X | X | +| Edit a post | X | X | +| Edit meta for a post | X | X | +| Delete a post | X | X | +| Delete meta for a post | X | ? | From 0aa84c45bfb804d0254751e5a454fab0bbbdb4da Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 13:12:16 +1000 Subject: [PATCH 178/515] Correct link format --- docs/comparison.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/comparison.md b/docs/comparison.md index d2bc690af2..ca484ee1ab 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -2,7 +2,7 @@ The purpose of this document is to compare the WordPress JSON REST API (WP API) to the other WordPress JSON API projects. Included in this comparison -are the (WordPress.com JSON REST API)[http://developer.wordpress.com/docs/api/], +are the [WordPress.com JSON REST API](http://developer.wordpress.com/docs/api/), and... ## Authentication From 36b78a922f8820895dd4f37a0772a5850c378ff0 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 13:12:27 +1000 Subject: [PATCH 179/515] Delete meta is available on dotcom Props @justinshreve for clarifying. --- docs/comparison.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/comparison.md b/docs/comparison.md index ca484ee1ab..919a7c4fba 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -43,4 +43,4 @@ and... | Edit a post | X | X | | Edit meta for a post | X | X | | Delete a post | X | X | -| Delete meta for a post | X | ? | +| Delete meta for a post | X | X | From 36194ab10ac883d499c913754e64bb223a52b9dd Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 13:14:45 +1000 Subject: [PATCH 180/515] Group meta comparison --- docs/comparison.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/comparison.md b/docs/comparison.md index 919a7c4fba..0c09e70d05 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -36,11 +36,11 @@ and... | Feature | WP API | WP.com JSON API | |:-------------------------|:-------|:---------------:| | Create a post | X | X | -| Create meta for a post | X | X | | List all posts | X | X | | Retrieve a post | X | X | -| Retrieve meta for a post | X | X | | Edit a post | X | X | -| Edit meta for a post | X | X | | Delete a post | X | X | +| Create meta for a post | X | X | +| Retrieve meta for a post | X | X | +| Edit meta for a post | X | X | | Delete meta for a post | X | X | From 8d56119b8c34f948de6da6c6498fb0d257ff6cb6 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 13:23:35 +1000 Subject: [PATCH 181/515] Add taxonomies comparison --- docs/comparison.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/comparison.md b/docs/comparison.md index 0c09e70d05..8b6cb94282 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -44,3 +44,18 @@ and... | Retrieve meta for a post | X | X | | Edit meta for a post | X | X | | Delete meta for a post | X | X | + +## Taxonomies + +| Feature | WP API | WP.com JSON API | +|:-------------------------|:-------|:---------------:| +| Create a term | | X | +| List all terms | X | X | +| Retrieve a term | X | X | +| Edit a term | | X | +| Delete a term | | X | +| Create a taxonomy | | | +| List all taxonomies | X | | +| Retrieve a taxonomy | X | | +| Edit a taxonomy | | | +| Delete a taxonomy | | | From 8d91d2eec973093afb95410f0237698772a35977 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 13:25:35 +1000 Subject: [PATCH 182/515] Add media comparison --- docs/comparison.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/comparison.md b/docs/comparison.md index 8b6cb94282..6183cbee1f 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -59,3 +59,13 @@ and... | Retrieve a taxonomy | X | | | Edit a taxonomy | | | | Delete a taxonomy | | | + +## Media + +| Feature | WP API | WP.com JSON API | +|:-------------------------|:-------|:---------------:| +| Create an attachment | X | X | +| List all attachments | X | X | +| Retrieve an attachment | X | X | +| Edit an attachment | X | X | +| Delete an attachment | X | X | From 8a31b03292e58f152d73e7259b6e5997c78c3494 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 13:31:24 +1000 Subject: [PATCH 183/515] Add comments comparison --- docs/comparison.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/comparison.md b/docs/comparison.md index 6183cbee1f..6ba7539777 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -45,6 +45,18 @@ and... | Edit meta for a post | X | X | | Delete meta for a post | X | X | +## Comments + +| Feature | WP API | WP.com JSON API | +|:-------------------------|:-------|:---------------:| +| Create a comment | | X | +| Create a reply | | X | +| List all comments | | X | +| List comments for a post | X | X | +| Retrieve a comment | X | X | +| Edit a comment | | X | +| Delete a comment | X | X | + ## Taxonomies | Feature | WP API | WP.com JSON API | From 240f93b80f17b59478ef0370446d9b4a39f432f3 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 13:52:40 +1000 Subject: [PATCH 184/515] Add further site comparison docs --- docs/comparison.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/comparison.md b/docs/comparison.md index 6ba7539777..a94e4322bc 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -18,6 +18,10 @@ and... | Feature | WP API | WP.com JSON API | |:-------------------------|:-------|:---------------:| | Basic Site Information | X | X | +| Retrieve options data | | X | +| Update options data | | | +| Retrieve post count | X | X | +| List available routes | X | ? | ## Users From fb13076502776e82f34715a60459adbd91ba9641 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 14:01:08 +1000 Subject: [PATCH 185/515] Add XML-RPC comparison --- docs/comparison.md | 121 +++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/docs/comparison.md b/docs/comparison.md index a94e4322bc..dcbda12897 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -3,85 +3,86 @@ The purpose of this document is to compare the WordPress JSON REST API (WP API) to the other WordPress JSON API projects. Included in this comparison are the [WordPress.com JSON REST API](http://developer.wordpress.com/docs/api/), -and... +and the built-in XML-RPC API. ## Authentication -| Feature | WP API | WP.com JSON API | -|:-------------------------|:-------|:----------------:| -| Cookie-based | X | | -| OAuth1 | X | | -| OAuth2 | | X | +| Feature | WP API | WP.com JSON API | XML-RPC API | +|:-------------------------|:-------|:----------------|:-----------:| +| Cookie-based | X | | | +| Basic authentication | P | | X | +| OAuth1 | X | | | +| OAuth2 | | X | | ## Site -| Feature | WP API | WP.com JSON API | -|:-------------------------|:-------|:---------------:| -| Basic Site Information | X | X | -| Retrieve options data | | X | -| Update options data | | | -| Retrieve post count | X | X | -| List available routes | X | ? | +| Feature | WP API | WP.com JSON API | XML-RPC API | +|:-------------------------|:-------|:----------------|:-----------:| +| Basic Site Information | X | X | M | +| Retrieve options data | | X | X | +| Update options data | | | X | +| Retrieve post count | X | X | | +| List available routes | X | - | X | ## Users -| Feature | WP API | WP.com JSON API | -|:-------------------------|:--------|:---------------:| -| Create a user | X | | -| List all users | X | X | -| Retrieve a user | X | | -| Retrieve current user | X | X | -| Edit a user | X | | -| Delete a user | X | | +| Feature | WP API | WP.com JSON API | XML-RPC API | +|:-------------------------|:-------|:----------------|:-----------:| +| Create a user | X | | | +| List all users | X | X | X | +| Retrieve a user | X | | X | +| Retrieve current user | X | X | X | +| Edit a user | X | | X | +| Delete a user | X | | | ## Posts -| Feature | WP API | WP.com JSON API | -|:-------------------------|:-------|:---------------:| -| Create a post | X | X | -| List all posts | X | X | -| Retrieve a post | X | X | -| Edit a post | X | X | -| Delete a post | X | X | -| Create meta for a post | X | X | -| Retrieve meta for a post | X | X | -| Edit meta for a post | X | X | -| Delete meta for a post | X | X | +| Feature | WP API | WP.com JSON API | XML-RPC API | +|:-------------------------|:-------|:----------------|:-----------:| +| Create a post | X | X | X | +| List all posts | X | X | X | +| Retrieve a post | X | X | X | +| Edit a post | X | X | X | +| Delete a post | X | X | X | +| Create meta for a post | X | X | X | +| Retrieve meta for a post | X | X | X | +| Edit meta for a post | X | X | X | +| Delete meta for a post | X | X | X | ## Comments -| Feature | WP API | WP.com JSON API | -|:-------------------------|:-------|:---------------:| -| Create a comment | | X | -| Create a reply | | X | -| List all comments | | X | -| List comments for a post | X | X | -| Retrieve a comment | X | X | -| Edit a comment | | X | -| Delete a comment | X | X | +| Feature | WP API | WP.com JSON API | XML-RPC API | +|:-------------------------|:-------|:----------------|:-----------:| +| Create a comment | | X | X | +| Create a reply | | X | X | +| List all comments | | X | X | +| List comments for a post | X | X | X | +| Retrieve a comment | X | X | X | +| Edit a comment | | X | X | +| Delete a comment | X | X | X | ## Taxonomies -| Feature | WP API | WP.com JSON API | -|:-------------------------|:-------|:---------------:| -| Create a term | | X | -| List all terms | X | X | -| Retrieve a term | X | X | -| Edit a term | | X | -| Delete a term | | X | -| Create a taxonomy | | | -| List all taxonomies | X | | -| Retrieve a taxonomy | X | | -| Edit a taxonomy | | | -| Delete a taxonomy | | | +| Feature | WP API | WP.com JSON API | XML-RPC API | +|:-------------------------|:-------|:---------------:|:-----------:| +| Create a term | | X | X | +| List all terms | X | X | X | +| Retrieve a term | X | X | X | +| Edit a term | | X | X | +| Delete a term | | X | X | +| Create a taxonomy | | | | +| List all taxonomies | X | | X | +| Retrieve a taxonomy | X | | X | +| Edit a taxonomy | | | | +| Delete a taxonomy | | | | ## Media -| Feature | WP API | WP.com JSON API | -|:-------------------------|:-------|:---------------:| -| Create an attachment | X | X | -| List all attachments | X | X | -| Retrieve an attachment | X | X | -| Edit an attachment | X | X | -| Delete an attachment | X | X | +| Feature | WP API | WP.com JSON API | XML-RPC API | +|:-------------------------|:-------|:---------------:|:-----------:| +| Create an attachment | X | X | X | +| List all attachments | X | X | X | +| Retrieve an attachment | X | X | X | +| Edit an attachment | X | X | X | +| Delete an attachment | X | X | X | From c171c9609d9c50ced7049d949b1a1a59060f62d2 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 14:05:34 +1000 Subject: [PATCH 186/515] Add key --- docs/comparison.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/comparison.md b/docs/comparison.md index dcbda12897..bb8c5f1a58 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -5,6 +5,11 @@ API) to the other WordPress JSON API projects. Included in this comparison are the [WordPress.com JSON REST API](http://developer.wordpress.com/docs/api/), and the built-in XML-RPC API. +## Key +* **M**: Multisite only +* **P**: Available via (official) plugin +* **~**: Partially available + ## Authentication | Feature | WP API | WP.com JSON API | XML-RPC API | @@ -22,7 +27,7 @@ and the built-in XML-RPC API. | Retrieve options data | | X | X | | Update options data | | | X | | Retrieve post count | X | X | | -| List available routes | X | - | X | +| List available routes | X | ~ | X | ## Users From bec33ddad8901b47d2fe7c9a89c3662323ff3b95 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 14:11:05 +1000 Subject: [PATCH 187/515] Add post periphery comparison --- docs/comparison.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/comparison.md b/docs/comparison.md index bb8c5f1a58..56b8c48ac2 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -67,6 +67,15 @@ and the built-in XML-RPC API. | Edit a comment | | X | X | | Delete a comment | X | X | X | +# Post-Related Data + +| Feature | WP API | WP.com JSON API | XML-RPC API | +|:-------------------------|:-------|:----------------|:-----------:| +| List all post types | X | | X | +| Retrieve post type | X | | X | +| List all post statuses | X | | X | +| Retrieve post status | | | | + ## Taxonomies | Feature | WP API | WP.com JSON API | XML-RPC API | From 7770e06c99926e1f820df81977c16b3e1e5d71ef Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 14:11:17 +1000 Subject: [PATCH 188/515] Add revision comparison --- docs/comparison.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/comparison.md b/docs/comparison.md index 56b8c48ac2..97ad8c7a28 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -91,6 +91,13 @@ and the built-in XML-RPC API. | Edit a taxonomy | | | | | Delete a taxonomy | | | | +## Revisions + +| Feature | WP API | WP.com JSON API | XML-RPC API | +|:-------------------------|:-------|:---------------:|:-----------:| +| Get revisions for a post | X | | X | +| Restore revision | | | X | + ## Media | Feature | WP API | WP.com JSON API | XML-RPC API | From c56a5ffbf830edcdb97e4875a0c9961bf38dcacd Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 14:20:37 +1000 Subject: [PATCH 189/515] Add custom authentication comparison --- docs/comparison.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/comparison.md b/docs/comparison.md index 97ad8c7a28..5838c454b2 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -18,6 +18,11 @@ and the built-in XML-RPC API. | Basic authentication | P | | X | | OAuth1 | X | | | | OAuth2 | | X | | +| Custom authentication | X | | ~ | + +Custom authentication can be used with WP API via the `determine_current_user` +filter. With the XML-RPC API, only one custom handler can be added at a time via +subclassing. ## Site From 9a549c8276d5f548e5ccef244837b350d2866f69 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 14:23:29 +1000 Subject: [PATCH 190/515] Add page comparison --- docs/comparison.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/comparison.md b/docs/comparison.md index 5838c454b2..82bbde5176 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -112,3 +112,14 @@ subclassing. | Retrieve an attachment | X | X | X | | Edit an attachment | X | X | X | | Delete an attachment | X | X | X | + +## Pages + +| Feature | WP API | WP.com JSON API | XML-RPC API | +|:-------------------------|:-------|:---------------:|:-----------:| +| Create a page | X | X | X | +| List all pages | X | X | X | +| Retrieve a page | X | X | X | +| Retrieve a page by slug | X | X | | +| Edit a page | X | X | X | +| Delete a page | X | X | X | From 4e39426cd8ba8d813ce601d62acf264e36a8a487 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 14:24:47 +1000 Subject: [PATCH 191/515] Add post retrieval-by-slug comparison --- docs/comparison.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/comparison.md b/docs/comparison.md index 82bbde5176..bc0929fd70 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -53,6 +53,7 @@ subclassing. | Create a post | X | X | X | | List all posts | X | X | X | | Retrieve a post | X | X | X | +| Retrieve a post by slug | | X | | | Edit a post | X | X | X | | Delete a post | X | X | X | | Create meta for a post | X | X | X | From 1213ca93cfb76093a54f511ee3f887650c8251b0 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 14:29:34 +1000 Subject: [PATCH 192/515] Add extra text --- docs/comparison.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/comparison.md b/docs/comparison.md index bc0929fd70..3e81961cfd 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -34,6 +34,8 @@ subclassing. | Retrieve post count | X | X | | | List available routes | X | ~ | X | +WordPress.com only shows some of the available routes on a request to the site's +index. ## Users @@ -97,6 +99,9 @@ subclassing. | Edit a taxonomy | | | | | Delete a taxonomy | | | | +Taxonomies are defined in code, so no APIs can create, modify, or delete +taxonomies. + ## Revisions | Feature | WP API | WP.com JSON API | XML-RPC API | From acf09d064d1b513b9f656f3b26760a67d1412aca Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 14:29:42 +1000 Subject: [PATCH 193/515] WP.com's API exposes extra data via /help --- docs/comparison.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/comparison.md b/docs/comparison.md index 3e81961cfd..c4def10675 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -32,10 +32,10 @@ subclassing. | Retrieve options data | | X | X | | Update options data | | | X | | Retrieve post count | X | X | | -| List available routes | X | ~ | X | +| List available routes | X | X | X | WordPress.com only shows some of the available routes on a request to the site's -index. +index. These are available via a separate `help` endpoint instead. ## Users From 5d55204034024762fe846baa4e9ffb02363e508e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 16:37:01 +1000 Subject: [PATCH 194/515] Set up and restore post data Turns out subtle errors turn up if we don't do this. *sigh* --- lib/class-wp-json-posts.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 941c79693e..3e448c04b0 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -612,6 +612,11 @@ protected function prepare_post( $post, $context = 'view' ) { if ( ! $this->check_read_permission( $post ) ) return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); + $previous_post = $GLOBALS['post']; + $post_obj = get_post( $post['ID'] ); + $GLOBALS['post'] = $post_obj; + setup_postdata( $post_obj ); + // prepare common post fields $post_fields = array( 'title' => get_the_title( $post['ID'] ), // $post['post_title'], @@ -685,17 +690,23 @@ protected function prepare_post( $post, $context = 'view' ) { if ( 'edit' === $context ) { if ( current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { if ( is_wp_error( $post_fields_raw['post_meta'] ) ) { + $GLOBALS['post'] = $previous_post; + setup_postdata( $previous_post ); return $post_fields_raw['post_meta']; } $_post = array_merge( $_post, $post_fields_raw ); } else { + $GLOBALS['post'] = $previous_post; + setup_postdata( $previous_post ); return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); } } elseif ( 'view-revision' == $context ) { if ( current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { $_post = array_merge( $_post, $post_fields_raw ); } else { + $GLOBALS['post'] = $previous_post; + setup_postdata( $previous_post ); return new WP_Error( 'json_cannot_view', __( 'Sorry, you cannot view this revision' ), array( 'status' => 403 ) ); } } @@ -717,6 +728,8 @@ protected function prepare_post( $post, $context = 'view' ) { if ( ! empty( $post['post_parent'] ) ) $_post['meta']['links']['up'] = json_url( '/posts/' . (int) $post['post_parent'] ); + $GLOBALS['post'] = $previous_post; + setup_postdata( $previous_post ); return apply_filters( 'json_prepare_post', $_post, $post, $context ); } From a12065dae055b8901f869608dedd6f00b74f470e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 16:37:50 +1000 Subject: [PATCH 195/515] Provide raw excerpt with edit context --- lib/class-wp-json-posts.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 3e448c04b0..1b70fb19ac 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -640,6 +640,7 @@ protected function prepare_post( $post, $context = 'view' ) { $post_fields_raw = array( 'title_raw' => $post['post_title'], 'content_raw' => $post['post_content'], + 'excerpt_raw' => $post['post_excerpt'], 'guid_raw' => $post['guid'], 'post_meta' => $this->get_all_meta( $post['ID'] ), ); From 6b5c739d9a9c32037383fb5b9e3e5cfe59e9a8fa Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 17:30:19 +1000 Subject: [PATCH 196/515] Create abstract base test class --- tests/bootstrap.php | 3 +++ tests/test_json_posts.php | 2 +- tests/test_json_posts_meta.php | 13 +------------ 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 5d8361551f..c17608ea0e 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -20,3 +20,6 @@ } else { require '../../../../tests/phpunit/includes/bootstrap.php'; } + +// Helper classes +require_once dirname( __FILE__ ) . '/test_json_testcase.php'; diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 429c29bdd8..4bc5def08a 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -6,7 +6,7 @@ * @package WordPress * @subpackage JSON API */ -class WP_Test_JSON_Posts extends WP_UnitTestCase { +class WP_Test_JSON_Posts extends WP_Test_JSON_TestCase { public function setUp() { parent::setUp(); diff --git a/tests/test_json_posts_meta.php b/tests/test_json_posts_meta.php index b8fd0e31a9..36c979f485 100644 --- a/tests/test_json_posts_meta.php +++ b/tests/test_json_posts_meta.php @@ -6,7 +6,7 @@ * @package WordPress * @subpackage JSON API */ -class WP_Test_JSON_Posts_Meta extends WP_UnitTestCase { +class WP_Test_JSON_Posts_Meta extends WP_Test_JSON_TestCase { public function setUp() { parent::setUp(); @@ -19,17 +19,6 @@ public function setUp() { $this->endpoint = new WP_JSON_Posts( $this->fake_server ); } - protected function assertErrorResponse( $code, $response, $status = null ) { - $this->assertInstanceOf( 'WP_Error', $response ); - $this->assertEquals( $code, $response->get_error_code() ); - - if ( $status !== null ) { - $data = $response->get_error_data(); - $this->assertArrayHasKey( 'status', $data ); - $this->assertEquals( $status, $data['status'] ); - } - } - public function test_get_meta() { $post_id = $this->factory->post->create(); $meta_id = add_post_meta( $post_id, 'testkey', 'testvalue' ); From 8b507a551865aba7c257fa26ead4ea1e23db300f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 17:42:08 +1000 Subject: [PATCH 197/515] Check correct date for creation --- tests/test_json_posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 4bc5def08a..57cfa7907e 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -60,7 +60,7 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi $timezone = $this->fake_server->get_timezone(); $post_date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_date, $timezone ); $post_date_gmt = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_date_gmt ); - $post_modified = $post_date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified, $timezone ); + $post_modified = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified, $timezone ); $post_modified_gmt = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified_gmt ); $this->assertEquals( $response_data['date'], $post_date->format( 'c' ) ); $this->assertEquals( $response_data['date_gmt'], $post_date_gmt->format( 'c' ) ); From 36cdb6c65af0c40cfcfbcc41e311ff12996dfba6 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 17:42:55 +1000 Subject: [PATCH 198/515] Check other post types --- tests/test_json_posts.php | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 57cfa7907e..40f5ea3645 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -104,6 +104,43 @@ function test_create_post() { $this->check_get_post_response( $response, $new_post ); } + function test_create_post_other_type() { + $data = $this->set_data(array( + 'type' => 'page', + )); + $response = $this->endpoint->new_post( $data ); + + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + $headers = $response->get_headers(); + + // Check that we succeeded + $this->assertEquals( 201, $response->get_status() ); + $this->assertArrayHasKey( 'Location', $headers ); + $this->assertArrayHasKey( 'Last-Modified', $headers ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + + $this->assertEquals( $data['title'], $new_post->post_title ); + $this->assertEquals( $data['type'], $new_post->post_type ); + $this->assertEquals( $data['content_raw'], $new_post->post_content ); + $this->assertEquals( $data['excerpt_raw'], $new_post->post_excerpt ); + $this->assertEquals( $data['name'], $new_post->post_name ); + $this->assertEquals( $data['status'], $new_post->post_status ); + $this->assertEquals( $data['author'], $new_post->post_author ); + + $this->check_get_post_response( $response, $new_post ); + } + + function test_create_post_invalid_type() { + $data = $this->set_data(array( + 'type' => 'testposttype', + )); + $response = $this->endpoint->new_post( $data ); + $this->assertErrorResponse( 'json_invalid_post_type', $response, 400 ); + } + function test_get_post() { $response = $this->endpoint->get_post( $this->post_id ); From 99c47a4e57070d2186cb6bec61dd187b45c7091f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 18:31:38 +1000 Subject: [PATCH 199/515] Add more post creation tests --- tests/test_json_posts.php | 130 +++++++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 37 deletions(-) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 40f5ea3645..c91d9dbeb8 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -76,12 +76,10 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi $this->assertEquals( $response_data['excerpt'], wpautop( $post_obj->post_excerpt ) ); $this->assertEquals( $response_data['guid'], $post_obj->guid ); - } - function test_create_post() { - $data = $this->set_data(); - $response = $this->endpoint->new_post( $data ); + } + protected function check_create_response( $response ) { $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); $headers = $response->get_headers(); @@ -94,51 +92,117 @@ function test_create_post() { $response_data = $response->get_data(); $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['title'], $new_post->post_title ); - $this->assertEquals( $data['content_raw'], $new_post->post_content ); - $this->assertEquals( $data['excerpt_raw'], $new_post->post_excerpt ); - $this->assertEquals( $data['name'], $new_post->post_name ); - $this->assertEquals( $data['status'], $new_post->post_status ); - $this->assertEquals( $data['author'], $new_post->post_author ); - $this->check_get_post_response( $response, $new_post ); } + function test_create_post() { + $data = $this->set_data(); + $response = $this->endpoint->new_post( $data ); + $this->check_create_response( $response ); + } + function test_create_post_other_type() { $data = $this->set_data(array( 'type' => 'page', )); $response = $this->endpoint->new_post( $data ); + $this->check_create_response( $response ); + } - $this->assertNotInstanceOf( 'WP_Error', $response ); + function test_create_post_invalid_type() { + $data = $this->set_data(array( + 'type' => 'testposttype', + )); + $response = $this->endpoint->new_post( $data ); + $this->assertErrorResponse( 'json_invalid_post_type', $response, 400 ); + } + + function test_create_post_other_author() { + $other_user = $this->factory->user->create( array( 'role' => 'author' ) ); + $data = $this->set_data(array( + 'author' => $other_user, + )); + $response = $this->endpoint->new_post( $data ); $response = json_ensure_response( $response ); - $headers = $response->get_headers(); + $this->check_create_response( $response ); - // Check that we succeeded - $this->assertEquals( 201, $response->get_status() ); - $this->assertArrayHasKey( 'Location', $headers ); - $this->assertArrayHasKey( 'Last-Modified', $headers ); + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['author'], $new_post->post_author ); + } + + function test_create_post_other_author_object() { + $other_user = $this->factory->user->create( array( 'role' => 'author' ) ); + $data = $this->set_data(array( + 'author' => (object) array( + 'ID' => $other_user, + ), + )); + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); $response_data = $response->get_data(); $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['author']->ID, $new_post->post_author ); + } - $this->assertEquals( $data['title'], $new_post->post_title ); - $this->assertEquals( $data['type'], $new_post->post_type ); - $this->assertEquals( $data['content_raw'], $new_post->post_content ); - $this->assertEquals( $data['excerpt_raw'], $new_post->post_excerpt ); - $this->assertEquals( $data['name'], $new_post->post_name ); - $this->assertEquals( $data['status'], $new_post->post_status ); - $this->assertEquals( $data['author'], $new_post->post_author ); + function test_create_post_invalid_author() { + $data = $this->set_data(array( + 'author' => -1, + )); + $response = $this->endpoint->new_post( $data ); + $this->assertErrorResponse( 'json_invalid_author', $response, 400 ); + } - $this->check_get_post_response( $response, $new_post ); + function test_create_post_invalid_author_object() { + $data = $this->set_data(array( + 'author' => (object) array( + 'ID' => -1, + ), + )); + $response = $this->endpoint->new_post( $data ); + $this->assertErrorResponse( 'json_invalid_author', $response, 400 ); } - function test_create_post_invalid_type() { + function test_create_post_invalid_author_without_permission() { + $data = $this->set_data(); + $other_user = $this->factory->user->create( array( 'role' => 'author' ) ); + + wp_set_current_user( $other_user ); + + $response = $this->endpoint->new_post( $data ); + $this->assertErrorResponse( 'json_cannot_edit_others', $response, 401 ); + } + + function test_create_post_private_without_permission() { $data = $this->set_data(array( - 'type' => 'testposttype', + 'status' => 'private', )); + $user = wp_get_current_user(); + $user->add_cap( 'publish_posts', false ); + + // Flush capabilities, https://core.trac.wordpress.org/ticket/28374 + $user->get_role_caps(); + $user->update_user_level_from_caps(); + $response = $this->endpoint->new_post( $data ); - $this->assertErrorResponse( 'json_invalid_post_type', $response, 400 ); + $this->assertErrorResponse( 'json_cannot_create_private', $response, 403 ); + } + + function test_create_post_publish_without_permission() { + $data = $this->set_data(array( + 'status' => 'publish', + )); + $user = wp_get_current_user(); + $user->add_cap( 'publish_posts', false ); + + // Flush capabilities, https://core.trac.wordpress.org/ticket/28374 + $user->get_role_caps(); + $user->update_user_level_from_caps(); + + $response = $this->endpoint->new_post( $data ); + $this->assertErrorResponse( 'json_cannot_publish', $response, 403 ); } function test_get_post() { @@ -169,15 +233,7 @@ function test_edit_post() { $edited_post = get_post( $this->post_id ); - // Check that the data has been updated - $this->assertEquals( $data['title'], $edited_post->post_title ); - $this->assertEquals( $data['content_raw'], $edited_post->post_content ); - $this->assertEquals( $data['excerpt_raw'], $edited_post->post_excerpt ); - $this->assertEquals( $data['name'], $edited_post->post_name ); - $this->assertEquals( $data['status'], $edited_post->post_status ); - $this->assertEquals( $data['author'], $edited_post->post_author ); - - $this->check_get_post_response( $response, $edited_post, 'edit' ); + $this->check_get_post_response( $response, $edited_post ); } From 43a39533e748357bced1a68e65ce5a1f83214532 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 18:33:02 +1000 Subject: [PATCH 200/515] Use (int) not absint for author sanitization --- lib/class-wp-json-posts.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 941c79693e..5bc4de104f 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1221,10 +1221,10 @@ protected function insert_post( $data ) { if ( empty( $data['author']->ID ) ) { return new WP_Error( 'json_invalid_author', __( 'Invalid author object.' ), array( 'status' => 400 ) ); } - $data['author'] = absint( $data['author']->ID ); + $data['author'] = (int) $data['author']->ID; } else { - $data['author'] = absint( $data['author'] ); + $data['author'] = (int) $data['author']; } // Only check edit others' posts if we are another user From 852fc95355a5f8d34309ad5c8d7456378265f35f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 18:33:38 +1000 Subject: [PATCH 201/515] Check _raw fields with edit context --- tests/test_json_posts.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index c91d9dbeb8..2f34d85030 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -76,6 +76,10 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi $this->assertEquals( $response_data['excerpt'], wpautop( $post_obj->post_excerpt ) ); $this->assertEquals( $response_data['guid'], $post_obj->guid ); + if ( $context === 'edit' ) { + $this->assertEquals( $response_data['content_raw'], $post_obj->post_content ); + $this->assertEquals( $response_data['excerpt_raw'], $post_obj->post_excerpt ); + } } From ce811f5f01116d8e6043185f9c6198850a41abb6 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 18:52:32 +1000 Subject: [PATCH 202/515] Add extra braces This helps coverage understand the code better, and fits with coding standards. --- lib/class-wp-json-posts.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 5bc4de104f..a918b62e67 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1144,13 +1144,16 @@ protected function insert_post( $data ) { // Permissions check if ( $update ) { - if ( ! current_user_can( $post_type->cap->edit_post, $data['ID'] ) ) + if ( ! current_user_can( $post_type->cap->edit_post, $data['ID'] ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => 401 ) ); - if ( $post_type->name != get_post_type( $data['ID'] ) ) + } + if ( $post_type->name != get_post_type( $data['ID'] ) ) { return new WP_Error( 'json_cannot_change_post_type', __( 'The post type may not be changed.' ), array( 'status' => 400 ) ); + } } else { - if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) + if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) { return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to post on this site.' ), array( 'status' => 400 ) ); + } } // Post status From c28b4676d3a050297cdd9e725581c82b69065864 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 18:53:01 +1000 Subject: [PATCH 203/515] Create post as test user --- tests/test_json_posts.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 2f34d85030..96418cd7b4 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -10,12 +10,12 @@ class WP_Test_JSON_Posts extends WP_Test_JSON_TestCase { public function setUp() { parent::setUp(); - $this->post_id = $this->factory->post->create(); - $this->post_obj = get_post( $this->post_id ); - $this->author_id = $this->factory->user->create( array( 'role' => 'editor' ) ); wp_set_current_user( $this->author_id ); + $this->post_id = $this->factory->post->create(); + $this->post_obj = get_post( $this->post_id ); + $this->fake_server = $this->getMock('WP_JSON_Server'); $this->endpoint = new WP_JSON_Posts( $this->fake_server ); } From af34612e3db999858e3d4897c89a302555a16e47 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 18:53:31 +1000 Subject: [PATCH 204/515] Test more post editing --- tests/test_json_posts.php | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 96418cd7b4..9e415d9731 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -240,8 +240,58 @@ function test_edit_post() { $this->check_get_post_response( $response, $edited_post ); } + function test_edit_post_without_permission() { + $data = $this->set_data( array( 'ID' => $this->post_id ) ) ; + + $user = wp_get_current_user(); + $user->add_cap( 'edit_published_posts', false ); + + // Flush capabilities, https://core.trac.wordpress.org/ticket/28374 + $user->get_role_caps(); + $user->update_user_level_from_caps(); + + $response = $this->endpoint->edit_post( $this->post_id, $data ); + $this->assertErrorResponse( 'json_cannot_edit', $response, 401 ); + } + + function test_edit_post_draft_without_permission() { + // Set post to draft status + wp_update_post(array( + 'ID' => $this->post_id, + 'post_status' => 'draft', + )); + $data = $this->set_data( array( 'ID' => $this->post_id ) ) ; + $user = wp_get_current_user(); + $user->add_cap( 'edit_posts', false ); + // Flush capabilities, https://core.trac.wordpress.org/ticket/28374 + $user->get_role_caps(); + $user->update_user_level_from_caps(); + + $response = $this->endpoint->edit_post( $this->post_id, $data ); + $this->assertErrorResponse( 'json_cannot_edit', $response, 401 ); + } + + function test_edit_post_change_type() { + $data = $this->set_data( array( + 'ID' => $this->post_id, + 'type' => 'page', + ) ) ; + + $response = $this->endpoint->edit_post( $this->post_id, $data ); + $this->assertErrorResponse( 'json_cannot_change_post_type', $response, 400 ); + } + + function test_edit_post_change_type_invalid() { + $data = $this->set_data( array( + 'ID' => $this->post_id, + 'type' => 'testposttype', + ) ) ; + + $response = $this->endpoint->edit_post( $this->post_id, $data ); + $this->assertErrorResponse( 'json_invalid_post_type', $response, 400 ); + } } From 33eacca50ba25e61dfe9619e99161da6c2a7e9ce Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 18:56:16 +1000 Subject: [PATCH 205/515] Test post creation permissions --- tests/test_json_posts.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 9e415d9731..89bf520c53 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -179,6 +179,19 @@ function test_create_post_invalid_author_without_permission() { $this->assertErrorResponse( 'json_cannot_edit_others', $response, 401 ); } + function test_create_post_without_permission() { + $data = $this->set_data(array()); + $user = wp_get_current_user(); + $user->add_cap( 'edit_posts', false ); + + // Flush capabilities, https://core.trac.wordpress.org/ticket/28374 + $user->get_role_caps(); + $user->update_user_level_from_caps(); + + $response = $this->endpoint->new_post( $data ); + $this->assertErrorResponse( 'json_cannot_create', $response, 403 ); + } + function test_create_post_private_without_permission() { $data = $this->set_data(array( 'status' => 'private', From 0b08c8c88caafb0c9fe15e384685c0a4b757c35a Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 18:56:37 +1000 Subject: [PATCH 206/515] Return 403 error if user can't create posts --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index a918b62e67..7a56bb741d 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1152,7 +1152,7 @@ protected function insert_post( $data ) { } } else { if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) { - return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to post on this site.' ), array( 'status' => 400 ) ); + return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to post on this site.' ), array( 'status' => 403 ) ); } } From dd99e90f16c4af898fd9967e09ed2eb50dfe7bfc Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 19:04:54 +1000 Subject: [PATCH 207/515] Check with invalid post author object --- tests/test_json_posts.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 89bf520c53..e0cef45f63 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -169,6 +169,16 @@ function test_create_post_invalid_author_object() { $this->assertErrorResponse( 'json_invalid_author', $response, 400 ); } + function test_create_post_invalid_author_object_id() { + $data = $this->set_data(array( + 'author' => (object) array( + 'testfield' => 'testvalue', + ), + )); + $response = $this->endpoint->new_post( $data ); + $this->assertErrorResponse( 'json_invalid_author', $response, 400 ); + } + function test_create_post_invalid_author_without_permission() { $data = $this->set_data(); $other_user = $this->factory->user->create( array( 'role' => 'author' ) ); From fe4bb020637c4607851bb77b3242195d999a3b92 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 19:16:33 +1000 Subject: [PATCH 208/515] Test creating draft and private posts --- tests/test_json_posts.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index e0cef45f63..3a1b10fe36 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -202,6 +202,34 @@ function test_create_post_without_permission() { $this->assertErrorResponse( 'json_cannot_create', $response, 403 ); } + function test_create_post_draft() { + $data = $this->set_data(array( + 'status' => 'draft', + )); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['status'], $new_post->post_status ); + } + + function test_create_post_private() { + $data = $this->set_data(array( + 'status' => 'private', + )); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['status'], $new_post->post_status ); + } + function test_create_post_private_without_permission() { $data = $this->set_data(array( 'status' => 'private', From 2bde2c21e722e3b2f71657587e195392ab4f7aa0 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 19:31:25 +1000 Subject: [PATCH 209/515] Give `null` as date for draft posts --- lib/class-wp-json-posts.php | 33 ++++++++++++++++++++++++--------- tests/test_json_posts.php | 30 ++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 7a56bb741d..c365560af1 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -642,15 +642,30 @@ protected function prepare_post( $post, $context = 'view' ) { // Dates $timezone = $this->server->get_timezone(); - $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_date'], $timezone ); - $post_fields['date'] = $date->format( 'c' ); - $post_fields_extended['date_tz'] = $date->format( 'e' ); - $post_fields_extended['date_gmt'] = date( 'c', strtotime( $post['post_date_gmt'] ) ); - - $modified = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_modified'], $timezone ); - $post_fields['modified'] = $modified->format( 'c' ); - $post_fields_extended['modified_tz'] = $modified->format( 'e' ); - $post_fields_extended['modified_gmt'] = date( 'c', strtotime( $post['post_modified_gmt'] ) ); + + if ( $post['post_date_gmt'] === '0000-00-00 00:00:00' ) { + $post_fields['date'] = null; + $post_fields_extended['date_tz'] = null; + $post_fields_extended['date_gmt'] = null; + } + else { + $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_date'], $timezone ); + $post_fields['date'] = $date->format( 'c' ); + $post_fields_extended['date_tz'] = $date->format( 'e' ); + $post_fields_extended['date_gmt'] = date( 'c', strtotime( $post['post_date_gmt'] ) ); + } + + if ( $post['post_modified_gmt'] === '0000-00-00 00:00:00' ) { + $post_fields['modified'] = null; + $post_fields_extended['modified_tz'] = null; + $post_fields_extended['modified_gmt'] = null; + } + else { + $modified = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_modified'], $timezone ); + $post_fields['modified'] = $modified->format( 'c' ); + $post_fields_extended['modified_tz'] = $modified->format( 'e' ); + $post_fields_extended['modified_gmt'] = date( 'c', strtotime( $post['post_modified_gmt'] ) ); + } // Authorized fields // TODO: Send `Vary: Authorization` to clarify that the data can be diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 3a1b10fe36..039c6af5cc 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -58,14 +58,28 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi // Check post dates. $timezone = $this->fake_server->get_timezone(); - $post_date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_date, $timezone ); - $post_date_gmt = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_date_gmt ); - $post_modified = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified, $timezone ); - $post_modified_gmt = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified_gmt ); - $this->assertEquals( $response_data['date'], $post_date->format( 'c' ) ); - $this->assertEquals( $response_data['date_gmt'], $post_date_gmt->format( 'c' ) ); - $this->assertEquals( $response_data['modified'], $post_modified->format( 'c' ) ); - $this->assertEquals( $response_data['modified_gmt'], $post_modified_gmt->format( 'c' ) ); + + if ( $post_obj->post_date_gmt === '0000-00-00 00:00:00' ) { + $this->assertNull( $response_data['date'] ); + $this->assertNull( $response_data['date_gmt'] ); + } + else { + $post_date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_date, $timezone ); + $post_date_gmt = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_date_gmt ); + $this->assertEquals( $response_data['date'], $post_date->format( 'c' ) ); + $this->assertEquals( $response_data['date_gmt'], $post_date_gmt->format( 'c' ) ); + } + + if ( $post_obj->post_modified_gmt === '0000-00-00 00:00:00' ) { + $this->assertNull( $response_data['modified'] ); + $this->assertNull( $response_data['modified_gmt'] ); + } + else { + $post_modified = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified, $timezone ); + $post_modified_gmt = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified_gmt ); + $this->assertEquals( $response_data['modified_gmt'], $post_modified_gmt->format( 'c' ) ); + $this->assertEquals( $response_data['modified'], $post_modified->format( 'c' ) ); + } // Check filtered values. From 36b065ccaae0d63a70733ee39163328d8848aa7f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 20:12:09 +1000 Subject: [PATCH 210/515] Test password-protected posts --- tests/test_json_posts.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 039c6af5cc..94e88a3422 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -274,6 +274,36 @@ function test_create_post_publish_without_permission() { $this->assertErrorResponse( 'json_cannot_publish', $response, 403 ); } + function test_create_post_with_password() { + $data = $this->set_data(array( + 'password' => 'testing', + )); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['password'], $new_post->post_password ); + } + + function test_create_post_with_password_without_permission() { + $data = $this->set_data(array( + 'password' => 'testing', + )); + + $user = wp_get_current_user(); + $user->add_cap( 'publish_posts', false ); + + // Flush capabilities, https://core.trac.wordpress.org/ticket/28374 + $user->get_role_caps(); + $user->update_user_level_from_caps(); + + $response = $this->endpoint->new_post( $data ); + $this->assertErrorResponse( 'json_cannot_publish', $response, 403 ); + } + function test_get_post() { $response = $this->endpoint->get_post( $this->post_id ); From bd5b90f9e790eef4dec281659c404201d2e1e87e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 20:31:10 +1000 Subject: [PATCH 211/515] Check page creation with parent --- tests/test_json_posts.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 94e88a3422..8152e12582 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -304,6 +304,30 @@ function test_create_post_with_password_without_permission() { $this->assertErrorResponse( 'json_cannot_publish', $response, 403 ); } + function test_create_page_with_parent() { + $this->markTestSkipped('https://github.com/WP-API/WP-API/issues/228'); + + $parent = $this->factory->post->create(array( + 'type' => 'page', + )); + $data = $this->set_data(array( + 'type' => 'page', + 'parent' => $parent, + )); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + + $this->assertInstanceOf( 'stdClass', $data['parent'] ); + $this->assertEquals( $data['parent']->ID, $new_post->post_parent ); + + $this->check_get_post_response( $data['parent'], get_post( $parent ) ); + } + function test_get_post() { $response = $this->endpoint->get_post( $this->post_id ); From 4b9fb1ae470b2fbb3ac8ecd4d20b25d3d53848d7 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 20:33:23 +1000 Subject: [PATCH 212/515] Partially revert "Give `null` as date for draft posts" This reverts commit 2bde2c21e722e3b2f71657587e195392ab4f7aa0. This is going to be fixed in #229 instead. --- lib/class-wp-json-posts.php | 33 +++++++++------------------------ tests/test_json_posts.php | 2 ++ 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index c365560af1..7a56bb741d 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -642,30 +642,15 @@ protected function prepare_post( $post, $context = 'view' ) { // Dates $timezone = $this->server->get_timezone(); - - if ( $post['post_date_gmt'] === '0000-00-00 00:00:00' ) { - $post_fields['date'] = null; - $post_fields_extended['date_tz'] = null; - $post_fields_extended['date_gmt'] = null; - } - else { - $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_date'], $timezone ); - $post_fields['date'] = $date->format( 'c' ); - $post_fields_extended['date_tz'] = $date->format( 'e' ); - $post_fields_extended['date_gmt'] = date( 'c', strtotime( $post['post_date_gmt'] ) ); - } - - if ( $post['post_modified_gmt'] === '0000-00-00 00:00:00' ) { - $post_fields['modified'] = null; - $post_fields_extended['modified_tz'] = null; - $post_fields_extended['modified_gmt'] = null; - } - else { - $modified = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_modified'], $timezone ); - $post_fields['modified'] = $modified->format( 'c' ); - $post_fields_extended['modified_tz'] = $modified->format( 'e' ); - $post_fields_extended['modified_gmt'] = date( 'c', strtotime( $post['post_modified_gmt'] ) ); - } + $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_date'], $timezone ); + $post_fields['date'] = $date->format( 'c' ); + $post_fields_extended['date_tz'] = $date->format( 'e' ); + $post_fields_extended['date_gmt'] = date( 'c', strtotime( $post['post_date_gmt'] ) ); + + $modified = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_modified'], $timezone ); + $post_fields['modified'] = $modified->format( 'c' ); + $post_fields_extended['modified_tz'] = $modified->format( 'e' ); + $post_fields_extended['modified_gmt'] = date( 'c', strtotime( $post['post_modified_gmt'] ) ); // Authorized fields // TODO: Send `Vary: Authorization` to clarify that the data can be diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 8152e12582..4c15bb187a 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -217,6 +217,8 @@ function test_create_post_without_permission() { } function test_create_post_draft() { + $this->markTestSkipped('https://github.com/WP-API/WP-API/issues/229'); + $data = $this->set_data(array( 'status' => 'draft', )); From 2bf3709435c521841642c06097dcbe309d73af11 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 20:37:19 +1000 Subject: [PATCH 213/515] Give `null` as date for draft posts --- lib/class-wp-json-posts.php | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 941c79693e..65a9cb10f0 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -642,15 +642,30 @@ protected function prepare_post( $post, $context = 'view' ) { // Dates $timezone = $this->server->get_timezone(); - $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_date'], $timezone ); - $post_fields['date'] = $date->format( 'c' ); - $post_fields_extended['date_tz'] = $date->format( 'e' ); - $post_fields_extended['date_gmt'] = date( 'c', strtotime( $post['post_date_gmt'] ) ); - - $modified = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_modified'], $timezone ); - $post_fields['modified'] = $modified->format( 'c' ); - $post_fields_extended['modified_tz'] = $modified->format( 'e' ); - $post_fields_extended['modified_gmt'] = date( 'c', strtotime( $post['post_modified_gmt'] ) ); + + if ( $post['post_date_gmt'] === '0000-00-00 00:00:00' ) { + $post_fields['date'] = null; + $post_fields_extended['date_tz'] = null; + $post_fields_extended['date_gmt'] = null; + } + else { + $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_date'], $timezone ); + $post_fields['date'] = $date->format( 'c' ); + $post_fields_extended['date_tz'] = $date->format( 'e' ); + $post_fields_extended['date_gmt'] = date( 'c', strtotime( $post['post_date_gmt'] ) ); + } + + if ( $post['post_modified_gmt'] === '0000-00-00 00:00:00' ) { + $post_fields['modified'] = null; + $post_fields_extended['modified_tz'] = null; + $post_fields_extended['modified_gmt'] = null; + } + else { + $modified = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_modified'], $timezone ); + $post_fields['modified'] = $modified->format( 'c' ); + $post_fields_extended['modified_tz'] = $modified->format( 'e' ); + $post_fields_extended['modified_gmt'] = date( 'c', strtotime( $post['post_modified_gmt'] ) ); + } // Authorized fields // TODO: Send `Vary: Authorization` to clarify that the data can be From 4f70d016aa2fabe26d57d35c1246af3f1061ea70 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 21:37:41 +1000 Subject: [PATCH 214/515] Correct post parent tests --- tests/test_json_posts.php | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 4c15bb187a..66dc72f4ae 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -34,13 +34,14 @@ protected function set_data( $args = array() ) { } protected function check_get_post_response( $response, $post_obj, $context = 'view' ) { + $response = json_ensure_response( $response ); $response_data = $response->get_data(); $this->assertEquals( $response_data['ID'], $post_obj->ID ); $this->assertEquals( $response_data['slug'], $post_obj->post_name ); $this->assertEquals( $response_data['status'], $post_obj->post_status ); $this->assertEquals( $response_data['author'], $post_obj->post_author ); - $this->assertEquals( $response_data['parent'], $post_obj->post_parent ); + $this->assertArrayHasKey( 'parent', $response_data ); $this->assertEquals( $response_data['link'], get_permalink( $post_obj->ID ) ); $this->assertEquals( $response_data['menu_order'], $post_obj->menu_order ); $this->assertEquals( $response_data['comment_status'], $post_obj->comment_status ); @@ -48,6 +49,20 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi $this->assertEquals( $response_data['password'], $post_obj->post_password ); $this->assertEquals( $response_data['sticky'], is_sticky( $post_obj->ID ) ); + // Check post parent. + if ( $post_obj->post_parent ) { + if ( is_int( $response_data['parent'] ) ) { + $this->assertEquals( $response_data['parent'], $post_obj->post_parent ); + } + else { + $this->assertEquals( $response_data['parent']['ID'], $post_obj->post_parent ); + $this->check_get_post_response( $response_data['parent'], get_post( $response_data['parent']['ID'] ), 'view-parent' ); + } + } + else { + $this->assertEmpty( $response_data['parent'] ); + } + // Check post format. $post_format = get_post_format( $post_obj->ID ); if ( empty( $post_format ) ) { @@ -324,10 +339,22 @@ function test_create_page_with_parent() { $response_data = $response->get_data(); $new_post = get_post( $response_data['ID'] ); - $this->assertInstanceOf( 'stdClass', $data['parent'] ); - $this->assertEquals( $data['parent']->ID, $new_post->post_parent ); + $this->assertEquals( $parent, $new_post->post_parent ); + } + + function test_create_page_with_invalid_parent() { + $this->markTestSkipped('https://github.com/WP-API/WP-API/issues/228'); + + $parent = $this->factory->post->create(array( + 'type' => 'page', + )); + $data = $this->set_data(array( + 'type' => 'page', + 'parent' => -1, + )); - $this->check_get_post_response( $data['parent'], get_post( $parent ) ); + $response = $this->endpoint->new_post( $data ); + $this->assertErrorResponse( 'json_post_invalid_id', $response, 400 ); } function test_get_post() { From bb8d74925c087a49c851fe43a763623af078f0c7 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 21:39:14 +1000 Subject: [PATCH 215/515] Use correct variable for parent ID --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 941c79693e..3c027c147d 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1259,7 +1259,7 @@ protected function insert_post( $data ) { // Parent if ( ! empty( $data['parent'] ) ) { $parent = get_post( $data['parent'] ); - $post['post_parent'] = $data['post_parent']; + $post['post_parent'] = $parent->ID; } // Menu order From 49e56d6bfb6880726e79eb253471692051ccf3ae Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 21:39:23 +1000 Subject: [PATCH 216/515] Check that parent ID is valid --- lib/class-wp-json-posts.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 3c027c147d..0b28eac412 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1259,6 +1259,10 @@ protected function insert_post( $data ) { // Parent if ( ! empty( $data['parent'] ) ) { $parent = get_post( $data['parent'] ); + if ( empty( $parent ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post parent ID.' ), array( 'status' => 400 ) ); + } + $post['post_parent'] = $parent->ID; } From 90f53720e0f3c088a59f5b22ae1a3ba4ad1c0ad9 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 21:48:47 +1000 Subject: [PATCH 217/515] Ensure assertions are always `$expected, $actual` --- tests/test_json_posts.php | 48 +++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 66dc72f4ae..f533ee3d3f 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -37,25 +37,25 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi $response = json_ensure_response( $response ); $response_data = $response->get_data(); - $this->assertEquals( $response_data['ID'], $post_obj->ID ); - $this->assertEquals( $response_data['slug'], $post_obj->post_name ); - $this->assertEquals( $response_data['status'], $post_obj->post_status ); - $this->assertEquals( $response_data['author'], $post_obj->post_author ); + $this->assertEquals( $post_obj->ID, $response_data['ID'] ); + $this->assertEquals( $post_obj->post_name, $response_data['slug'] ); + $this->assertEquals( $post_obj->post_status, $response_data['status'] ); + $this->assertEquals( $post_obj->post_author, $response_data['author'] ); $this->assertArrayHasKey( 'parent', $response_data ); - $this->assertEquals( $response_data['link'], get_permalink( $post_obj->ID ) ); - $this->assertEquals( $response_data['menu_order'], $post_obj->menu_order ); - $this->assertEquals( $response_data['comment_status'], $post_obj->comment_status ); - $this->assertEquals( $response_data['ping_status'], $post_obj->ping_status ); - $this->assertEquals( $response_data['password'], $post_obj->post_password ); - $this->assertEquals( $response_data['sticky'], is_sticky( $post_obj->ID ) ); + $this->assertEquals( get_permalink( $post_obj->ID ), $response_data['link'] ); + $this->assertEquals( $post_obj->menu_order, $response_data['menu_order'] ); + $this->assertEquals( $post_obj->comment_status, $response_data['comment_status'] ); + $this->assertEquals( $post_obj->ping_status, $response_data['ping_status'] ); + $this->assertEquals( $post_obj->post_password, $response_data['password'] ); + $this->assertEquals( is_sticky( $post_obj->ID ), $response_data['sticky'] ); // Check post parent. if ( $post_obj->post_parent ) { if ( is_int( $response_data['parent'] ) ) { - $this->assertEquals( $response_data['parent'], $post_obj->post_parent ); + $this->assertEquals( $post_obj->post_parent, $response_data['parent'] ); } else { - $this->assertEquals( $response_data['parent']['ID'], $post_obj->post_parent ); + $this->assertEquals( $post_obj->post_parent, $response_data['parent']['ID'] ); $this->check_get_post_response( $response_data['parent'], get_post( $response_data['parent']['ID'] ), 'view-parent' ); } } @@ -66,9 +66,9 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi // Check post format. $post_format = get_post_format( $post_obj->ID ); if ( empty( $post_format ) ) { - $this->assertEquals( $response_data['format'], 'standard' ); + $this->assertEquals( 'standard', $response_data['format'] ); } else { - $this->assertEquals( $response_data['format'], get_post_format( $post_obj->ID ) ); + $this->assertEquals( get_post_format( $post_obj->ID ), $response_data['format'] ); } // Check post dates. @@ -81,8 +81,8 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi else { $post_date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_date, $timezone ); $post_date_gmt = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_date_gmt ); - $this->assertEquals( $response_data['date'], $post_date->format( 'c' ) ); - $this->assertEquals( $response_data['date_gmt'], $post_date_gmt->format( 'c' ) ); + $this->assertEquals( $post_date->format( 'c' ), $response_data['date'] ); + $this->assertEquals( $post_date_gmt->format( 'c' ), $response_data['date_gmt'] ); } if ( $post_obj->post_modified_gmt === '0000-00-00 00:00:00' ) { @@ -92,22 +92,22 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi else { $post_modified = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified, $timezone ); $post_modified_gmt = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified_gmt ); - $this->assertEquals( $response_data['modified_gmt'], $post_modified_gmt->format( 'c' ) ); - $this->assertEquals( $response_data['modified'], $post_modified->format( 'c' ) ); + $this->assertEquals( $post_modified_gmt->format( 'c' ), $response_data['modified_gmt'] ); + $this->assertEquals( $post_modified->format( 'c' ), $response_data['modified'] ); } // Check filtered values. - $this->assertEquals( $response_data['title'], get_the_title( $post_obj->ID ) ); + $this->assertEquals( get_the_title( $post_obj->ID ), $response_data['title'] ); // TODO: apply content filter for more accurate testing. - $this->assertEquals( $response_data['content'], wpautop( $post_obj->post_content ) ); + $this->assertEquals( wpautop( $post_obj->post_content ), $response_data['content'] ); // TODO: apply excerpt filter for more accurate testing. - $this->assertEquals( $response_data['excerpt'], wpautop( $post_obj->post_excerpt ) ); - $this->assertEquals( $response_data['guid'], $post_obj->guid ); + $this->assertEquals( wpautop( $post_obj->post_excerpt ), $response_data['excerpt'] ); + $this->assertEquals( $post_obj->guid, $response_data['guid'] ); if ( $context === 'edit' ) { - $this->assertEquals( $response_data['content_raw'], $post_obj->post_content ); - $this->assertEquals( $response_data['excerpt_raw'], $post_obj->post_excerpt ); + $this->assertEquals( $post_obj->post_content, $response_data['content_raw'] ); + $this->assertEquals( $post_obj->post_excerpt, $response_data['excerpt_raw'] ); } } From e470fdd1a81926100eb082bc949a93b22043aaaf Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 27 May 2014 22:01:31 +1000 Subject: [PATCH 218/515] Only restore previous post if we have one --- lib/class-wp-json-posts.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 1b70fb19ac..f090820a2b 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -612,7 +612,10 @@ protected function prepare_post( $post, $context = 'view' ) { if ( ! $this->check_read_permission( $post ) ) return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); - $previous_post = $GLOBALS['post']; + $previous_post = null; + if ( ! empty( $GLOBALS['post'] ) ) { + $previous_post = $GLOBALS['post']; + } $post_obj = get_post( $post['ID'] ); $GLOBALS['post'] = $post_obj; setup_postdata( $post_obj ); @@ -692,14 +695,18 @@ protected function prepare_post( $post, $context = 'view' ) { if ( current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { if ( is_wp_error( $post_fields_raw['post_meta'] ) ) { $GLOBALS['post'] = $previous_post; - setup_postdata( $previous_post ); + if ( $previous_post ) { + setup_postdata( $previous_post ); + } return $post_fields_raw['post_meta']; } $_post = array_merge( $_post, $post_fields_raw ); } else { $GLOBALS['post'] = $previous_post; - setup_postdata( $previous_post ); + if ( $previous_post ) { + setup_postdata( $previous_post ); + } return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); } } elseif ( 'view-revision' == $context ) { @@ -707,7 +714,9 @@ protected function prepare_post( $post, $context = 'view' ) { $_post = array_merge( $_post, $post_fields_raw ); } else { $GLOBALS['post'] = $previous_post; - setup_postdata( $previous_post ); + if ( $previous_post ) { + setup_postdata( $previous_post ); + } return new WP_Error( 'json_cannot_view', __( 'Sorry, you cannot view this revision' ), array( 'status' => 403 ) ); } } @@ -730,7 +739,9 @@ protected function prepare_post( $post, $context = 'view' ) { $_post['meta']['links']['up'] = json_url( '/posts/' . (int) $post['post_parent'] ); $GLOBALS['post'] = $previous_post; - setup_postdata( $previous_post ); + if ( $previous_post ) { + setup_postdata( $previous_post ); + } return apply_filters( 'json_prepare_post', $_post, $post, $context ); } From 894ed23a85701be7d03cb8558439962aa1d99292 Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 27 May 2014 08:25:25 -0600 Subject: [PATCH 219/515] Coding standards plugin.php. --- plugin.php | 70 +++++++++++++++++++++++++----------------------------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/plugin.php b/plugin.php index 3571b043fc..25f8001516 100644 --- a/plugin.php +++ b/plugin.php @@ -13,7 +13,7 @@ * * @var string */ -define('JSON_API_VERSION', '1.0'); +define( 'JSON_API_VERSION', '1.0' ); /** * Include our files for the API @@ -41,7 +41,7 @@ function json_api_init() { json_api_register_rewrites(); global $wp; - $wp->add_query_var('json_route'); + $wp->add_query_var( 'json_route' ); } add_action( 'init', 'json_api_init' ); @@ -69,36 +69,36 @@ function json_api_maybe_flush_rewrites() { * * @internal This will live in default-filters.php */ -function json_api_default_filters($server) { +function json_api_default_filters( $server ) { global $wp_json_posts, $wp_json_pages, $wp_json_media, $wp_json_taxonomies; // Posts - $wp_json_posts = new WP_JSON_Posts($server); + $wp_json_posts = new WP_JSON_Posts( $server ); add_filter( 'json_endpoints', array( $wp_json_posts, 'register_routes' ), 0 ); // Users $wp_json_users = new WP_JSON_Users($server); - add_filter( 'json_endpoints', array( $wp_json_users, 'register_routes' ), 0 ); - add_filter( 'json_prepare_post', array( $wp_json_users, 'add_post_author_data' ), 10, 3 ); + add_filter( 'json_endpoints', array( $wp_json_users, 'register_routes' ), 0 ); + add_filter( 'json_prepare_post', array( $wp_json_users, 'add_post_author_data' ), 10, 3 ); add_filter( 'json_prepare_comment', array( $wp_json_users, 'add_comment_author_data' ), 10, 3 ); // Pages - $wp_json_pages = new WP_JSON_Pages($server); + $wp_json_pages = new WP_JSON_Pages( $server ); $wp_json_pages->register_filters(); // Media - $wp_json_media = new WP_JSON_Media($server); - add_filter( 'json_endpoints', array( $wp_json_media, 'register_routes' ), 1 ); + $wp_json_media = new WP_JSON_Media( $server ); + add_filter( 'json_endpoints', array( $wp_json_media, 'register_routes' ), 1 ); add_filter( 'json_prepare_post', array( $wp_json_media, 'add_thumbnail_data' ), 10, 3 ); - add_filter( 'json_pre_insert_post', array( $wp_json_media, 'preinsert_check' ), 10, 3 ); - add_filter( 'json_insert_post', array( $wp_json_media, 'attach_thumbnail' ), 10, 3 ); - add_filter( 'json_post_type_data', array( $wp_json_media, 'type_archive_link' ), 10, 2 ); + add_filter( 'json_pre_insert_post', array( $wp_json_media, 'preinsert_check' ), 10, 3 ); + add_filter( 'json_insert_post', array( $wp_json_media, 'attach_thumbnail' ), 10, 3 ); + add_filter( 'json_post_type_data', array( $wp_json_media, 'type_archive_link' ), 10, 2 ); // Posts $wp_json_taxonomies = new WP_JSON_Taxonomies($server); - add_filter( 'json_endpoints', array( $wp_json_taxonomies, 'register_routes' ), 2 ); + add_filter( 'json_endpoints', array( $wp_json_taxonomies, 'register_routes' ), 2 ); add_filter( 'json_post_type_data', array( $wp_json_taxonomies, 'add_taxonomy_data' ), 10, 2 ); - add_filter( 'json_prepare_post', array( $wp_json_taxonomies, 'add_term_data' ), 10, 3 ); + add_filter( 'json_prepare_post', array( $wp_json_taxonomies, 'add_term_data' ), 10, 3 ); } add_action( 'wp_json_server_before_serve', 'json_api_default_filters', 10, 1 ); @@ -120,19 +120,19 @@ function json_api_loaded() { * @var bool * @todo Remove me in favour of JSON_REQUEST */ - define('XMLRPC_REQUEST', true); + define( 'XMLRPC_REQUEST', true ); /** * Whether this is a JSON Request * * @var bool */ - define('JSON_REQUEST', true); + define( 'JSON_REQUEST', true ); global $wp_json_server; // Allow for a plugin to insert a different class to handle requests. - $wp_json_server_class = apply_filters('wp_json_server_class', 'WP_JSON_Server'); + $wp_json_server_class = apply_filters( 'wp_json_server_class', 'WP_JSON_Server' ); $wp_json_server = new $wp_json_server_class; /** @@ -144,7 +144,7 @@ function json_api_loaded() { * * @param WP_JSON_ResponseHandler $wp_json_server Response handler object */ - do_action('wp_json_server_before_serve', $wp_json_server); + do_action( 'wp_json_server_before_serve', $wp_json_server ); // Fire off the request $wp_json_server->serve_request( $GLOBALS['wp']->query_vars['json_route'] ); @@ -159,20 +159,17 @@ function json_api_loaded() { */ function json_api_activation( $network_wide ) { if ( function_exists( 'is_multisite' ) && is_multisite() && $network_wide ) { - $mu_blogs = wp_get_sites(); foreach ( $mu_blogs as $mu_blog ) { - switch_to_blog( $mu_blog['blog_id'] ); + json_api_register_rewrites(); update_option( 'json_api_plugin_version', null ); } restore_current_blog(); - } else { - json_api_register_rewrites(); update_option( 'json_api_plugin_version', null ); } @@ -188,15 +185,12 @@ function json_api_deactivation( $network_wide ) { $mu_blogs = wp_get_sites(); foreach ( $mu_blogs as $mu_blog ) { - switch_to_blog( $mu_blog['blog_id'] ); delete_option( 'json_api_plugin_version' ); } restore_current_blog(); - } else { - delete_option( 'json_api_plugin_version' ); } } @@ -215,9 +209,9 @@ function json_register_scripts() { * Add the API URL to the WP RSD endpoint */ function json_output_rsd() { -?> + ?> -\n"; } @@ -238,15 +233,17 @@ function json_output_link_wp_head() { * Send a Link header for the API */ function json_output_link_header() { - if ( headers_sent() ) + if ( headers_sent() ) { return; + } $api_root = get_json_url(); - if ( empty($api_root) ) + if ( empty($api_root) ) { return; + } - header('Link: <' . $api_root . '>; rel="https://github.com/WP-API/WP-API"', false); + header( 'Link: <' . $api_root . '>; rel="https://github.com/WP-API/WP-API"', false ); } add_action( 'template_redirect', 'json_output_link_header', 11, 0 ); @@ -309,8 +306,7 @@ function json_cookie_check_errors( $result ) { $nonce = null; if ( isset( $_REQUEST['_wp_json_nonce'] ) ) { $nonce = $_REQUEST['_wp_json_nonce']; - } - elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) { + } elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) { $nonce = $_SERVER['HTTP_X_WP_NONCE']; } @@ -369,16 +365,14 @@ function get_json_url( $blog_id = null, $path = '', $scheme = 'json' ) { if ( get_option( 'permalink_structure' ) ) { $url = get_home_url( $blog_id, 'wp-json', $scheme ); - if ( !empty( $path ) && is_string( $path ) && strpos( $path, '..' ) === false ) + if ( ! empty( $path ) && is_string( $path ) && strpos( $path, '..' ) === false ) $url .= '/' . ltrim( $path, '/' ); - } - else { + } else { $url = trailingslashit( get_home_url( $blog_id, '', $scheme ) ); if ( empty( $path ) ) { $path = '/'; - } - else { + } else { $path = '/' . ltrim( $path, '/' ); } From 198125a1087e9eeb2662d6944cc7df36aa2b3bc5 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 28 May 2014 00:31:38 +1000 Subject: [PATCH 220/515] Add missing base testcase Duh. --- tests/test_json_testcase.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/test_json_testcase.php diff --git a/tests/test_json_testcase.php b/tests/test_json_testcase.php new file mode 100644 index 0000000000..3cf235158c --- /dev/null +++ b/tests/test_json_testcase.php @@ -0,0 +1,14 @@ +assertInstanceOf( 'WP_Error', $response ); + $this->assertEquals( $code, $response->get_error_code() ); + + if ( $status !== null ) { + $data = $response->get_error_data(); + $this->assertArrayHasKey( 'status', $data ); + $this->assertEquals( $status, $data['status'] ); + } + } +} \ No newline at end of file From 52f3bdf235de102b73ed7aca5a256d32eb9077bf Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 28 May 2014 01:14:52 +1000 Subject: [PATCH 221/515] Note XML-RPC can only give limited subset of options --- docs/comparison.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/comparison.md b/docs/comparison.md index c4def10675..d585ccd67b 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -29,13 +29,13 @@ subclassing. | Feature | WP API | WP.com JSON API | XML-RPC API | |:-------------------------|:-------|:----------------|:-----------:| | Basic Site Information | X | X | M | -| Retrieve options data | | X | X | -| Update options data | | | X | +| Retrieve options data | | X | ~ | +| Update options data | | | ~ | | Retrieve post count | X | X | | | List available routes | X | X | X | -WordPress.com only shows some of the available routes on a request to the site's -index. These are available via a separate `help` endpoint instead. +The XML-RPC API only exposes a limited subset of the available options in +WordPress. ## Users From d856f00d94ebec76cb06db08dfaadf314908f558 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 28 May 2014 01:15:08 +1000 Subject: [PATCH 222/515] Remove empty taxonomy rows --- docs/comparison.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/comparison.md b/docs/comparison.md index d585ccd67b..12346199a9 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -93,11 +93,8 @@ WordPress. | Retrieve a term | X | X | X | | Edit a term | | X | X | | Delete a term | | X | X | -| Create a taxonomy | | | | | List all taxonomies | X | | X | | Retrieve a taxonomy | X | | X | -| Edit a taxonomy | | | | -| Delete a taxonomy | | | | Taxonomies are defined in code, so no APIs can create, modify, or delete taxonomies. From cec65dff941d6391eef821fc198a187981930f04 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 28 May 2014 01:20:26 +1000 Subject: [PATCH 223/515] Note that we do have support for retrieval by slug --- docs/comparison.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/comparison.md b/docs/comparison.md index 12346199a9..de5ce49f5b 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -55,7 +55,7 @@ WordPress. | Create a post | X | X | X | | List all posts | X | X | X | | Retrieve a post | X | X | X | -| Retrieve a post by slug | | X | | +| Retrieve a post by slug | ~ | X | | | Edit a post | X | X | X | | Delete a post | X | X | X | | Create meta for a post | X | X | X | @@ -63,6 +63,11 @@ WordPress. | Edit meta for a post | X | X | X | | Delete meta for a post | X | X | X | +WP API does not provide a specialised page-by-slug retrieval method, but allows +accessing via WP Query: + + GET /wp-json/posts?filter[name]=my-post-slug + ## Comments | Feature | WP API | WP.com JSON API | XML-RPC API | From ea234eade32939497541f94c791f03d369bcfe67 Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Fri, 23 May 2014 11:09:47 -0400 Subject: [PATCH 224/515] Add /taxonomies routes It is frequently desirable to know about all taxonomies, regardless of the post type to which they are associated; it is also convenient to be able to retrieve taxonomies from a base-level endpoint. This adds the following routes to the taxonomies class: - /taxonomies - /taxonomies/(?P\w+) - /taxonomies/(?P\w+)/terms - /taxonomies/(?P\w+)/terms/(?P\w+) Bug Reference: #198 --- lib/class-wp-json-taxonomies.php | 48 ++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 51f7fc9537..530d00df9b 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -21,6 +21,18 @@ public function register_routes( $routes ) { '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms/(?P\w+)' => array( array( array( $this, 'get_term' ), WP_JSON_Server::READABLE ), ), + '/taxonomies' => array( + array( array( $this, 'get_taxonomies' ), WP_JSON_Server::READABLE ), + ), + '/taxonomies/(?P\w+)' => array( + array( array( $this, 'get_taxonomy' ), WP_JSON_Server::READABLE ), + ), + '/taxonomies/(?P\w+)/terms' => array( + array( array( $this, 'get_terms' ), WP_JSON_Server::READABLE ), + ), + '/taxonomies/(?P\w+)/terms/(?P\w+)' => array( + array( array( $this, 'get_term' ), WP_JSON_Server::READABLE ), + ), ); return array_merge( $routes, $tax_routes ); } @@ -28,11 +40,15 @@ public function register_routes( $routes ) { /** * Get taxonomies * - * @param string $type Post type to get taxonomies for + * @param string $type Post type to get taxonomies for (optional) * @return array Taxonomy data */ - public function get_taxonomies( $type ) { - $taxonomies = get_object_taxonomies( $type, 'objects' ); + public function get_taxonomies( $type = '' ) { + if ( $type !== '' ) { + $taxonomies = get_object_taxonomies( $type, 'objects' ); + } else { + $taxonomies = get_taxonomies( '', 'objects' ); + } $data = array(); foreach ($taxonomies as $tax_type => $value) { @@ -49,10 +65,11 @@ public function get_taxonomies( $type ) { /** * Get taxonomies * - * @param string $type Post type to get taxonomies for + * @param string $taxonomy Taxonomy slug + * @param string $type Post type to get taxonomies for (optional) * @return array Taxonomy data */ - public function get_taxonomy( $type, $taxonomy ) { + public function get_taxonomy( $taxonomy, $type = '' ) { $tax = get_taxonomy( $taxonomy ); if ( empty( $tax ) ) return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); @@ -64,6 +81,7 @@ public function get_taxonomy( $type, $taxonomy ) { * Prepare a taxonomy for serialization * * @param stdClass $taxonomy Taxonomy data + * @param string $type Post type to get taxonomies for * @param boolean $_in_collection Are we in a collection? * @return array Taxonomy data */ @@ -71,7 +89,11 @@ protected function prepare_taxonomy( $taxonomy, $type, $_in_collection = false ) if ( $taxonomy->public === false ) return new WP_Error( 'json_cannot_read_taxonomy', __( 'Cannot view taxonomy' ), array( 'status' => 403 ) ); - $base_url = '/posts/types/' . $type . '/taxonomies/' . $taxonomy->name; + if ( $type !== '' ) { + $base_url = '/posts/types/' . $type . '/taxonomies/' . $taxonomy->name; + } else { + $base_url = '/taxonomies/' . $taxonomy->name; + } $data = array( 'name' => $taxonomy->label, @@ -113,11 +135,11 @@ public function add_taxonomy_data( $data, $type ) { /** * Get terms for a post type * - * @param string $type Post type * @param string $taxonomy Taxonomy slug + * @param string $type Post type (optional) * @return array Term collection */ - public function get_terms( $type, $taxonomy ) { + public function get_terms( $taxonomy, $type = '' ) { if ( ! taxonomy_exists( $taxonomy ) ) return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); @@ -138,13 +160,13 @@ public function get_terms( $type, $taxonomy ) { /** * Get term for a post type * - * @param string $type Post type * @param string $taxonomy Taxonomy slug * @param string $term Term slug + * @param string $type Post type (optional) * @param string $context Context (view/view-parent) * @return array Term entity */ - public function get_term( $type, $taxonomy, $term, $context = 'view' ) { + public function get_term( $taxonomy, $term, $type = '', $context = 'view' ) { if ( ! taxonomy_exists( $taxonomy ) ) return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); @@ -181,7 +203,11 @@ public function add_term_data( $data, $post, $context ) { * @return array The prepared term data */ protected function prepare_term( $term, $type, $context = 'view' ) { - $base_url = '/posts/types/' . $type . '/taxonomies/' . $term->taxonomy . '/terms'; + if ( $type !== '' ) { + $base_url = '/posts/types/' . $type . '/taxonomies/' . $term->taxonomy . '/terms'; + } else { + $base_url = '/taxonomies/' . $term->taxonomy . '/terms'; + } $data = array( 'ID' => (int) $term->term_taxonomy_id, 'name' => $term->name, From 6d727447790cced0d99ee291ce8d0a7334c1dc28 Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 27 May 2014 10:27:49 -0600 Subject: [PATCH 225/515] Coding standards lib/class-wp-json-server.php. --- lib/class-wp-json-server.php | 125 ++++++++++++++++++++--------------- 1 file changed, 73 insertions(+), 52 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 540e636d0d..3748b7f71a 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -7,7 +7,7 @@ * @package WordPress */ -require_once ABSPATH . 'wp-admin/includes/admin.php'; +require_once ( ABSPATH . 'wp-admin/includes/admin.php' ); /** * WordPress JSON API server handler @@ -133,8 +133,7 @@ protected function error_to_response( $error ) { $error_data = $error->get_error_data(); if ( is_array( $error_data ) && isset( $error_data['status'] ) ) { $status = $error_data['status']; - } - else { + } else { $status = 500; } @@ -164,10 +163,11 @@ protected function error_to_response( $error ) { * @return string JSON representation of the error */ protected function json_error( $code, $message, $status = null ) { - if ( $status ) + if ( $status ) { $this->set_status( $status ); - + } $error = compact( 'code', 'message' ); + return json_encode( array( $error ) ); } @@ -184,13 +184,14 @@ public function serve_request( $path = null ) { // Proper filter for turning off the JSON API. It is on by default. $enabled = apply_filters( 'json_enabled', true ); + $jsonp_enabled = apply_filters( 'json_jsonp_enabled', true ); if ( ! $enabled ) { echo $this->json_error( 'json_disabled', __( 'The JSON API is disabled on this site.' ), 404 ); return false; } - if ( isset($_GET['_jsonp']) ) { + if ( isset( $_GET['_jsonp'] ) ) { if ( ! $jsonp_enabled ) { echo $this->json_error( 'json_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 ); return false; @@ -204,18 +205,19 @@ public function serve_request( $path = null ) { } if ( empty( $path ) ) { - if ( isset( $_SERVER['PATH_INFO'] ) ) + if ( isset( $_SERVER['PATH_INFO'] ) ) { $path = $_SERVER['PATH_INFO']; - else + } else { $path = '/'; + } } - $this->path = $path; - $this->method = $_SERVER['REQUEST_METHOD']; - $this->params['GET'] = $_GET; + $this->path = $path; + $this->method = $_SERVER['REQUEST_METHOD']; + $this->params['GET'] = $_GET; $this->params['POST'] = $_POST; - $this->headers = $this->get_headers( $_SERVER ); - $this->files = $_FILES; + $this->headers = $this->get_headers( $_SERVER ); + $this->files = $_FILES; // Compatibility for clients that can't use PUT/PATCH/DELETE if ( isset( $_GET['_method'] ) ) { @@ -259,15 +261,17 @@ public function serve_request( $path = null ) { $served = apply_filters( 'json_serve_request', false, $result, $path, $this->method, $this ); if ( ! $served ) { - if ( 'HEAD' === $this->method ) + if ( 'HEAD' === $this->method ) { return; + } $result = json_encode( $this->prepare_response( $result ) ); - if ( isset($_GET['_jsonp']) ) + if ( isset( $_GET['_jsonp'] ) ) { echo $_GET['_jsonp'] . '(' . $result . ')'; - else + } else { echo $result; + } } } @@ -337,34 +341,41 @@ public function dispatch() { default: return new WP_Error( 'json_unsupported_method', __( 'Unsupported request method' ), array( 'status' => 400 ) ); } + foreach ( $this->get_routes() as $route => $handlers ) { foreach ( $handlers as $handler ) { $callback = $handler[0]; $supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET; - if ( !( $supported & $method ) ) + if ( ! ( $supported & $method ) ) { continue; + } $match = preg_match( '@^' . $route . '$@i', $this->path, $args ); - if ( !$match ) + if ( ! $match ) { continue; + } - if ( ! is_callable( $callback ) ) + if ( ! is_callable( $callback ) ) { return new WP_Error( 'json_invalid_handler', __( 'The handler for the route is invalid' ), array( 'status' => 500 ) ); + } $args = array_merge( $args, $this->params['GET'] ); + if ( $method & self::METHOD_POST ) { $args = array_merge( $args, $this->params['POST'] ); } + if ( $supported & self::ACCEPT_JSON ) { $data = json_decode( $this->get_raw_data(), true ); + if ( $data !== null ) { $args = array_merge( $args, array( 'data' => $data ) ); } - } - elseif ( $supported & self::ACCEPT_RAW ) { + } elseif ( $supported & self::ACCEPT_RAW ) { $data = $this->get_raw_data(); + if ( ! empty( $data ) ) { $args = array_merge( $args, array( 'data' => $data ) ); } @@ -384,8 +395,10 @@ public function dispatch() { } $params = $this->sort_callback_params( $callback, $args ); - if ( is_wp_error( $params ) ) + + if ( is_wp_error( $params ) ) { return $params; + } return call_user_func_array( $callback, $params ); } @@ -405,10 +418,11 @@ public function dispatch() { * @return array */ protected function sort_callback_params( $callback, $provided ) { - if ( is_array( $callback ) ) + if ( is_array( $callback ) ) { $ref_func = new ReflectionMethod( $callback[0], $callback[1] ); - else + } else { $ref_func = new ReflectionFunction( $callback ); + } $wanted = $ref_func->getParameters(); $ordered_parameters = array(); @@ -417,12 +431,10 @@ protected function sort_callback_params( $callback, $provided ) { if ( isset( $provided[ $param->getName() ] ) ) { // We have this parameters in the list to choose from $ordered_parameters[] = $provided[ $param->getName() ]; - } - elseif ( $param->isDefaultValueAvailable() ) { + } elseif ( $param->isDefaultValueAvailable() ) { // We don't have this parameter, but it's optional $ordered_parameters[] = $param->getDefaultValue(); - } - else { + } else { // We don't have this parameter and it wasn't optional, abort! return new WP_Error( 'json_missing_callback_param', sprintf( __( 'Missing parameter %s' ), $param->getName() ), array( 'status' => 400 ) ); } @@ -442,14 +454,14 @@ protected function sort_callback_params( $callback, $provided ) { public function get_index() { // General site data $available = array( - 'name' => get_option( 'blogname' ), - 'description' => get_option( 'blogdescription' ), - 'URL' => get_option( 'siteurl' ), - 'routes' => array(), + 'name' => get_option( 'blogname' ), + 'description' => get_option( 'blogdescription' ), + 'URL' => get_option( 'siteurl' ), + 'routes' => array(), 'authentication' => array(), - 'meta' => array( + 'meta' => array( 'links' => array( - 'help' => 'https://github.com/WP-API/WP-API', + 'help' => 'https://github.com/WP-API/WP-API', 'profile' => 'https://raw.github.com/WP-API/WP-API/master/docs/schema.json', ), ), @@ -461,17 +473,21 @@ public function get_index() { $route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route ); $methods = array(); + foreach ( self::$method_map as $name => $bitmask ) { foreach ( $callbacks as $callback ) { // Skip to the next route if any callback is hidden - if ( $callback[1] & self::HIDDEN_ENDPOINT ) + if ( $callback[1] & self::HIDDEN_ENDPOINT ) { continue 3; + } - if ( $callback[1] & $bitmask ) + if ( $callback[1] & $bitmask ) { $data['supports'][] = $name; + } - if ( $callback[1] & self::ACCEPT_JSON ) + if ( $callback[1] & self::ACCEPT_JSON ) { $data['accepts_json'] = true; + } // For non-variable routes, generate links if ( strpos( $route, '<' ) === false ) { @@ -481,7 +497,7 @@ public function get_index() { } } } - $available['routes'][$route] = apply_filters( 'json_endpoints_description', $data ); + $available['routes'][ $route ] = apply_filters( 'json_endpoints_description', $data ); } return apply_filters( 'json_index', $available ); } @@ -546,7 +562,7 @@ public function get_raw_data() { * @param mixed $data Native representation * @return array|string Data ready for `json_encode()` */ - public function prepare_response($data) { + public function prepare_response( $data ) { if ( ! defined( 'WP_JSON_SERIALIZE_COMPATIBLE' ) || WP_JSON_SERIALIZE_COMPATIBLE === false ) { return $data; } @@ -567,8 +583,7 @@ public function prepare_response($data) { case 'object': if ( $data instanceof JsonSerializable ) { $data = $data->jsonSerialize(); - } - else { + } else { $data = get_object_vars( $data ); } @@ -591,6 +606,7 @@ public function prepare_response($data) { public function parse_date( $date, $force_utc = false ) { // Default timezone to the server's current one $timezone = self::get_timezone(); + if ( $force_utc ) { $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date ); $timezone = new DateTimeZone( 'UTC' ); @@ -614,6 +630,7 @@ public function parse_date( $date, $force_utc = false ) { */ public function get_date_with_gmt( $date, $force_utc = false ) { $datetime = $this->parse_date( $date, $force_utc ); + if ( empty( $datetime ) ) { return null; } @@ -642,7 +659,6 @@ public function get_avatar_url( $email ) { preg_match('/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches); if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) { - return esc_url_raw( $matches[1] ); } @@ -656,21 +672,26 @@ public function get_avatar_url( $email ) { */ public function get_timezone() { static $zone = null; - if ($zone !== null) + + if ( $zone !== null ) { return $zone; + } $tzstring = get_option( 'timezone_string' ); + if ( ! $tzstring ) { // Create a UTC+- zone if no timezone string exists $current_offset = get_option( 'gmt_offset' ); - if ( 0 == $current_offset ) + if ( 0 == $current_offset ) { $tzstring = 'UTC'; - elseif ($current_offset < 0) + } elseif ( $current_offset < 0 ) { $tzstring = 'Etc/GMT' . $current_offset; - else + } else { $tzstring = 'Etc/GMT+' . $current_offset; + } } $zone = new DateTimeZone( $tzstring ); + return $zone; } @@ -680,16 +701,16 @@ public function get_timezone() { * @param array $server Associative array similar to $_SERVER * @return array Headers extracted from the input */ - public function get_headers($server) { + public function get_headers( $server ) { $headers = array(); + // CONTENT_* headers are not prefixed with HTTP_ - $additional = array('CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true); + $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true ); - foreach ($server as $key => $value) { - if ( strpos( $key, 'HTTP_' ) === 0) { + foreach ( $server as $key => $value ) { + if ( strpos( $key, 'HTTP_' ) === 0 ) { $headers[ substr( $key, 5 ) ] = $value; - } - elseif ( isset( $additional[ $key ] ) ) { + } elseif ( isset( $additional[ $key ] ) ) { $headers[ $key ] = $value; } } From 7873d098b20e81ad455f7a3910e22ba9275d015f Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 27 May 2014 10:36:28 -0600 Subject: [PATCH 226/515] Coding standards lib/class-wp-json-customposttype.php. --- lib/class-wp-json-customposttype.php | 30 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/class-wp-json-customposttype.php b/lib/class-wp-json-customposttype.php index c4af4da48c..ec2a0f5315 100644 --- a/lib/class-wp-json-customposttype.php +++ b/lib/class-wp-json-customposttype.php @@ -111,8 +111,9 @@ public function register_comment_routes( $routes ) { * @see WP_JSON_Posts::get_posts() */ public function get_posts( $filter = array(), $context = 'view', $type = null, $page = 1 ) { - if ( !empty( $type ) && $type !== $this->type ) + if ( ! empty( $type ) && $type !== $this->type ) { return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); + } return parent::get_posts( $filter, $context, $this->type, $page ); } @@ -125,13 +126,15 @@ public function get_posts( $filter = array(), $context = 'view', $type = null, $ public function get_post( $id, $context = 'view' ) { $id = (int) $id; - if ( empty( $id ) ) + if ( empty( $id ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } $post = get_post( $id, ARRAY_A ); - if ( $post['post_type'] !== $this->type ) + if ( $post['post_type'] !== $this->type ) { return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); + } return parent::get_post( $id, $context ); } @@ -143,16 +146,20 @@ public function get_post( $id, $context = 'view' ) { */ function edit_post( $id, $data, $_headers = array() ) { $id = (int) $id; - if ( empty( $id ) ) + + if ( empty( $id ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) + if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } - if ( $post['post_type'] !== $this->type ) + if ( $post['post_type'] !== $this->type ) { return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); + } return parent::edit_post( $id, $data, $_headers ); } @@ -165,13 +172,15 @@ function edit_post( $id, $data, $_headers = array() ) { public function delete_post( $id, $force = false ) { $id = (int) $id; - if ( empty( $id ) ) + if ( empty( $id ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } $post = get_post( $id, ARRAY_A ); - if ( $post['post_type'] !== $this->type ) + if ( $post['post_type'] !== $this->type ) { return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); + } return parent::delete_post( $id, $force ); } @@ -197,8 +206,9 @@ protected function prepare_post( $post, $context = 'view' ) { ), ); - if ( ! empty( $post['post_parent'] ) ) + if ( ! empty( $post['post_parent'] ) ) { $_post['meta']['links']['up'] = json_url( $this->base . '/' . $post['ID'] ); + } return apply_filters( 'json_prepare_{$this->type}', $_post, $post, $context ); } @@ -218,4 +228,4 @@ public function type_archive_link( $data, $type ) { $data['meta']['links']['archives'] = json_url( $this->base ); return $data; } -} \ No newline at end of file +} From bad9d136736daf0a04e7d0d99ae8a22584b579f7 Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 27 May 2014 10:37:24 -0600 Subject: [PATCH 227/515] Coding standards lib/class-wp-json-datetime.php. --- lib/class-wp-json-datetime.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-datetime.php b/lib/class-wp-json-datetime.php index ae04419b72..9cd852392b 100644 --- a/lib/class-wp-json-datetime.php +++ b/lib/class-wp-json-datetime.php @@ -16,14 +16,15 @@ class WP_JSON_DateTime extends DateTime { * @param DateTimeZone $timezone A DateTimeZone object representing the desired time zone. * @return Datetime */ - public static function createFromFormat($format, $time, $timezone = null ) { + public static function createFromFormat( $format, $time, $timezone = null ) { if ( is_null( $timezone ) ) { $timezone = new DateTimeZone( date_default_timezone_get() ); } + if ( method_exists( 'DateTime', 'createFromFormat' ) ) { return parent::createFromFormat( $format, $time, $timezone ); } return new DateTime( date( $format, strtotime( $time ) ), $timezone ); } -} \ No newline at end of file +} From 1d3527b0ecfd5b5edea78c13d34e7c7a6b5d6616 Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Tue, 27 May 2014 12:44:28 -0400 Subject: [PATCH 228/515] typo (inforcement > enforcement) --- docs/guides/extending.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/extending.md b/docs/guides/extending.md index 8f7a3692a2..9348e9765e 100644 --- a/docs/guides/extending.md +++ b/docs/guides/extending.md @@ -41,7 +41,7 @@ publishing a post is an action, it's equivalent to updating a post's status. Keep in mind that since the REST API is opinionated, you will have to work with the API's structure rather than rolling your own. The API is strict about how you talk to the client, and you should always use the built-in handlers rather -than rolling your own. This strict inforcement of data handling helps to make +than rolling your own. This strict enforcement of data handling helps to make the API more pluggable and extensible, plus ensures that you play nice with other plugins. From 0d2771c3e90e91646e5c1ff5505dffda37e6f0fc Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 27 May 2014 10:44:29 -0600 Subject: [PATCH 229/515] Coding standards lib/class-wp-json-media.php. --- lib/class-wp-json-media.php | 119 ++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 46 deletions(-) diff --git a/lib/class-wp-json-media.php b/lib/class-wp-json-media.php index 4f1c7bd0c3..b6180fa97e 100644 --- a/lib/class-wp-json-media.php +++ b/lib/class-wp-json-media.php @@ -9,11 +9,10 @@ class WP_JSON_Media extends WP_JSON_Posts { */ public function register_routes( $routes ) { $media_routes = array( - '/media' => array( + '/media' => array( array( array( $this, 'get_posts' ), WP_JSON_Server::READABLE ), array( array( $this, 'upload_attachment' ), WP_JSON_Server::CREATABLE ), ), - '/media/(?P\d+)' => array( array( array( $this, 'get_post' ), WP_JSON_Server::READABLE ), array( array( $this, 'edit_post' ), WP_JSON_Server::EDITABLE ), @@ -32,8 +31,9 @@ public function register_routes( $routes ) { * @see WP_JSON_Posts::get_posts() */ public function get_posts( $filter = array(), $context = 'view', $type = 'attachment', $page = 1 ) { - if ( $type !== 'attachment' ) + if ( $type !== 'attachment' ) { return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); + } if ( empty( $filter['post_status'])) { $filter['post_status'] = array( 'publish', 'inherit' ); @@ -68,13 +68,15 @@ public function allow_status_query( $vars ) { public function get_post( $id, $context = 'view' ) { $id = (int) $id; - if ( empty( $id ) ) + if ( empty( $id ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } $post = get_post( $id, ARRAY_A ); - if ( $post['post_type'] !== 'attachment' ) + if ( $post['post_type'] !== 'attachment' ) { return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); + } return parent::get_post( $id, $context ); } @@ -88,20 +90,19 @@ public function get_post( $id, $context = 'view' ) { protected function prepare_post( $post, $context = 'single' ) { $data = parent::prepare_post( $post, $context ); - if ( is_wp_error( $data ) || $post['post_type'] !== 'attachment' ) + if ( is_wp_error( $data ) || $post['post_type'] !== 'attachment' ) { return $data; + } // $thumbnail_size = current_theme_supports( 'post-thumbnail' ) ? 'post-thumbnail' : 'thumbnail'; - $data['source'] = wp_get_attachment_url( $post['ID'] ); - $data['is_image'] = wp_attachment_is_image( $post['ID'] ); - + $data['source'] = wp_get_attachment_url( $post['ID'] ); + $data['is_image'] = wp_attachment_is_image( $post['ID'] ); $data['attachment_meta'] = wp_get_attachment_metadata( $post['ID'] ); // Ensure empty meta is an empty object - if ( empty( $data['attachment_meta'] ) ){ + if ( empty( $data['attachment_meta'] ) ) { $data['attachment_meta'] = new stdClass; - } - elseif ( ! empty( $data['attachment_meta']['sizes'] ) ) { + } elseif ( ! empty( $data['attachment_meta']['sizes'] ) ) { $img_url_basename = wp_basename( $data['source'] ); foreach ($data['attachment_meta']['sizes'] as $size => &$size_data) { @@ -121,8 +122,9 @@ protected function prepare_post( $post, $context = 'single' ) { ), ); - if ( ! empty( $post['post_parent'] ) ) + if ( ! empty( $post['post_parent'] ) ) { $data['meta']['links']['up'] = json_url( '/media/' . (int) $post['post_parent'] ); + } return apply_filters( 'json_prepare_attachment', $data, $post, $context ); } @@ -134,16 +136,20 @@ protected function prepare_post( $post, $context = 'single' ) { */ public function edit_post( $id, $data, $_headers = array() ) { $id = (int) $id; - if ( empty( $id ) ) + + if ( empty( $id ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) + if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } - if ( $post['post_type'] !== 'attachment' ) + if ( $post['post_type'] !== 'attachment' ) { return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); + } return parent::edit_post( $id, $data, $_headers ); } @@ -156,13 +162,15 @@ public function edit_post( $id, $data, $_headers = array() ) { public function delete_post( $id, $force = false ) { $id = (int) $id; - if ( empty( $id ) ) + if ( empty( $id ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } $post = get_post( $id, ARRAY_A ); - if ( $post['post_type'] !== 'attachment' ) + if ( $post['post_type'] !== 'attachment' ) { return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); + } return parent::delete_post( $id, $force ); } @@ -180,63 +188,72 @@ public function delete_post( $id, $force = false ) { */ public function upload_attachment( $_files, $_headers ) { $post_type = get_post_type_object( 'attachment' ); - if ( ! $post_type ) + + if ( ! $post_type ) { return new WP_Error( 'json_invalid_post_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); + } // Permissions check - if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) + if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) { return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to post on this site.' ), array( 'status' => 400 ) ); + } // Get the file via $_FILES or raw data if ( empty( $_files ) ) { $file = $this->upload_from_data( $_files, $_headers ); - } - else { + } else { $file = $this->upload_from_file( $_files, $_headers ); } - if ( is_wp_error( $file ) ) + if ( is_wp_error( $file ) ) { return $file; + } - $name = basename( $file['file'] ); + $name = basename( $file['file'] ); $name_parts = pathinfo( $name ); - $name = trim( substr( $name, 0, -(1 + strlen($name_parts['extension'])) ) ); + $name = trim( substr( $name, 0, -(1 + strlen($name_parts['extension'])) ) ); - $url = $file['url']; - $type = $file['type']; - $file = $file['file']; - $title = $name; + $url = $file['url']; + $type = $file['type']; + $file = $file['file']; + $title = $name; $content = ''; // use image exif/iptc data for title and caption defaults if possible if ( $image_meta = @wp_read_image_metadata($file) ) { - if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) + if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { $title = $image_meta['title']; - if ( trim( $image_meta['caption'] ) ) + } + + if ( trim( $image_meta['caption'] ) ) { $content = $image_meta['caption']; + } } // Construct the attachment array - $post_data = array(); + $post_data = array(); $attachment = array( 'post_mime_type' => $type, - 'guid' => $url, + 'guid' => $url, // 'post_parent' => $post_id, - 'post_title' => $title, - 'post_content' => $content, + 'post_title' => $title, + 'post_content' => $content, ); // This should never be set as it would then overwrite an existing attachment. - if ( isset( $attachment['ID'] ) ) + if ( isset( $attachment['ID'] ) ) { unset( $attachment['ID'] ); + } // Save the data $id = wp_insert_attachment($attachment, $file /*, $post_id */); + if ( !is_wp_error($id) ) { wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) ); } $headers = array( 'Location' => json_url( '/media/' . $id ) ); + return new WP_JSON_Response( $this->get_post( $id, 'edit' ), 201, $headers ); } @@ -263,16 +280,18 @@ protected function upload_from_data( $_files, $_headers ) { } // Get the filename - $disposition_parts = explode(';', $_headers['CONTENT_DISPOSITION']); + $disposition_parts = explode( ';', $_headers['CONTENT_DISPOSITION'] ); $filename = null; - foreach ($disposition_parts as $part) { - $part = trim($part); - if (strpos($part, 'filename') !== 0) + foreach ( $disposition_parts as $part ) { + $part = trim( $part ); + + if ( strpos( $part, 'filename' ) !== 0 ) { continue; + } - $filenameparts = explode('=', $part); - $filename = trim($filenameparts[1]); + $filenameparts = explode( '=', $part ); + $filename = trim( $filenameparts[1] ); } if ( empty( $filename ) ) { @@ -281,7 +300,8 @@ protected function upload_from_data( $_files, $_headers ) { if ( ! empty( $_headers['CONTENT_MD5'] ) ) { $expected = trim( $_headers['CONTENT_MD5'] ); - $actual = md5( $data ); + $actual = md5( $data ); + if ( $expected !== $actual ) { return new WP_Error( 'json_upload_hash_mismatch', __( 'Content hash did not match expected' ), array( 'status' => 412 ) ); } @@ -294,6 +314,7 @@ protected function upload_from_data( $_files, $_headers ) { $tmpfname = wp_tempnam( $filename ); $fp = fopen( $tmpfname, 'w+' ); + if ( ! $fp ) { return new WP_Error( 'json_upload_file_error', __( 'Could not open file handle' ), array( 'status' => 500 ) ); } @@ -345,10 +366,12 @@ protected function upload_from_file( $_files, $_headers ) { $overrides = array( 'test_form' => false, ); + $file = wp_handle_upload( $_files['file'], $overrides ); - if ( isset($file['error']) ) + if ( isset( $file['error'] ) ) { return new WP_Error( 'json_upload_unknown_error', $file['error'], array( 'status' => 500 ) ); + } return $file; } @@ -369,6 +392,7 @@ public function preinsert_check( $status, $post, $data ) { // Featured image (pre-verification) if ( ! empty( $data['featured_image'] ) ) { $thumbnail = $this->get_post( (int) $data['featured_image'], 'child' ); + if ( is_wp_error( $thumbnail ) ) { return new WP_Error( 'json_invalid_featured_image', __( 'Invalid featured image.' ), array( 'status' => 400 ) ); } @@ -392,6 +416,7 @@ public function attach_thumbnail( $post, $data, $update ) { if ( ! empty( $data['featured_image'] ) ) { // Already verified in preinsert_check() $thumbnail = $this->get_post( $data['featured_image'], 'child' ); + set_post_thumbnail( $post['ID'], $thumbnail['ID'] ); } } @@ -405,7 +430,7 @@ public function attach_thumbnail( $post, $data, $update ) { * @return array Filtered post data */ public function add_thumbnail_data( $data, $post, $context ) { - if( !post_type_supports( $post['post_type'], 'thumbnail' ) ) { + if ( ! post_type_supports( $post['post_type'], 'thumbnail' ) ) { return $data; } @@ -413,6 +438,7 @@ public function add_thumbnail_data( $data, $post, $context ) { if ( $context !== 'view' && $context !== 'edit' ) { $data['featured_image'] = null; $thumbnail_id = get_post_thumbnail_id( $post['ID'] ); + if ( $thumbnail_id ) { $data['featured_image'] = absint( $thumbnail_id ); } @@ -423,6 +449,7 @@ public function add_thumbnail_data( $data, $post, $context ) { // Thumbnail $data['featured_image'] = null; $thumbnail_id = get_post_thumbnail_id( $post['ID'] ); + if ( $thumbnail_id ) { $data['featured_image'] = $this->get_post( $thumbnail_id, 'child' ); } @@ -445,4 +472,4 @@ public function type_archive_link( $data, $type ) { $data['meta']['links']['archives'] = json_url( '/media' ); return $data; } -} \ No newline at end of file +} From 4727a1f813e7fa915aba883e279898ea1e3346eb Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 27 May 2014 10:46:34 -0600 Subject: [PATCH 230/515] Coding standards lib/class-wp-json-pages.php. --- lib/class-wp-json-pages.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-json-pages.php b/lib/class-wp-json-pages.php index 50d3ff2ab7..df73f61eca 100644 --- a/lib/class-wp-json-pages.php +++ b/lib/class-wp-json-pages.php @@ -62,8 +62,9 @@ public function register_routes( $routes ) { public function get_post_by_path( $path, $context = 'view' ) { $post = get_page_by_path( $path, ARRAY_A ); - if ( empty( $post ) ) + if ( empty( $post ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } return $this->get_post( $post['ID'], $context ); } @@ -76,8 +77,9 @@ public function get_post_by_path( $path, $context = 'view' ) { public function edit_post_by_path( $path, $data, $_headers = array() ) { $post = get_page_by_path( $path, ARRAY_A ); - if ( empty( $post ) ) + if ( empty( $post ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } return $this->edit_post( $post['ID'], $data, $_headers ); } @@ -90,8 +92,9 @@ public function edit_post_by_path( $path, $data, $_headers = array() ) { public function delete_post_by_path( $path, $force = false ) { $post = get_page_by_path( $path, ARRAY_A ); - if ( empty( $post ) ) + if ( empty( $post ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } return $this->delete_post( $post['ID'], $force ); } @@ -109,9 +112,10 @@ protected function prepare_post( $post, $context = 'view' ) { // Override entity meta keys with the correct links $_post['meta']['links']['self'] = json_url( $this->base . '/' . get_page_uri( $post['ID'] ) ); - if ( ! empty( $post['post_parent'] ) ) + if ( ! empty( $post['post_parent'] ) ) { $_post['meta']['links']['up'] = json_url( $this->base . '/' . get_page_uri( (int) $post['post_parent'] ) ); + } return apply_filters( 'json_prepare_page', $_post, $post, $context ); } -} \ No newline at end of file +} From e7d562c9227405c6907dc9670ea8e307ae1b9c0f Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 27 May 2014 11:15:13 -0600 Subject: [PATCH 231/515] Coding standards lib/class-wp-json-posts.php. --- lib/class-wp-json-posts.php | 391 +++++++++++++++++++++++------------- 1 file changed, 248 insertions(+), 143 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 941c79693e..2448ec5014 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -26,42 +26,50 @@ public function __construct(WP_JSON_ResponseHandler $server) { public function register_routes( $routes ) { $post_routes = array( // Post endpoints - '/posts' => array( - array( array( $this, 'get_posts' ), WP_JSON_Server::READABLE ), - array( array( $this, 'new_post' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), + '/posts' => array( + array( array( $this, 'get_posts' ), WP_JSON_Server::READABLE ), + array( array( $this, 'new_post' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), ), '/posts/(?P\d+)' => array( - array( array( $this, 'get_post' ), WP_JSON_Server::READABLE ), - array( array( $this, 'edit_post' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), - array( array( $this, 'delete_post' ), WP_JSON_Server::DELETABLE ), + array( array( $this, 'get_post' ), WP_JSON_Server::READABLE ), + array( array( $this, 'edit_post' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), + array( array( $this, 'delete_post' ), WP_JSON_Server::DELETABLE ), + ), + '/posts/(?P\d+)/revisions' => array( + array( $this, 'get_revisions' ), WP_JSON_Server::READABLE ), - '/posts/(?P\d+)/revisions' => array( array( $this, 'get_revisions' ), WP_JSON_Server::READABLE ), // Meta '/posts/(?P\d+)/meta' => array( - array( array( $this, 'get_all_meta' ), WP_JSON_Server::READABLE ), - array( array( $this, 'add_meta' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), + array( array( $this, 'get_all_meta' ), WP_JSON_Server::READABLE ), + array( array( $this, 'add_meta' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), ), '/posts/(?P\d+)/meta/(?P\d+)' => array( - array( array( $this, 'get_meta' ), WP_JSON_Server::READABLE ), - array( array( $this, 'update_meta' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), - array( array( $this, 'delete_meta' ), WP_JSON_Server::DELETABLE ), + array( array( $this, 'get_meta' ), WP_JSON_Server::READABLE ), + array( array( $this, 'update_meta' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), + array( array( $this, 'delete_meta' ), WP_JSON_Server::DELETABLE ), ), // Comments - '/posts/(?P\d+)/comments' => array( - array( array( $this, 'get_comments' ), WP_JSON_Server::READABLE ), + '/posts/(?P\d+)/comments' => array( + array( array( $this, 'get_comments' ), WP_JSON_Server::READABLE ), ), '/posts/(?P\d+)/comments/(?P\d+)' => array( - array( array( $this, 'get_comment' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_comment' ), WP_JSON_Server::READABLE ), array( array( $this, 'delete_comment' ), WP_JSON_Server::DELETABLE ), ), // Meta-post endpoints - '/posts/types' => array( array( $this, 'get_post_types' ), WP_JSON_Server::READABLE ), - '/posts/types/(?P\w+)' => array( array( $this, 'get_post_type' ), WP_JSON_Server::READABLE ), - '/posts/statuses' => array( array( $this, 'get_post_statuses' ), WP_JSON_Server::READABLE ), + '/posts/types' => array( + array( $this, 'get_post_types' ), WP_JSON_Server::READABLE + ), + '/posts/types/(?P\w+)' => array( + array( $this, 'get_post_type' ), WP_JSON_Server::READABLE + ), + '/posts/statuses' => array( + array( $this, 'get_post_statuses' ), WP_JSON_Server::READABLE + ), ); return array_merge( $routes, $post_routes ); } @@ -76,8 +84,9 @@ public function register_routes( $routes ) { public function get_revisions( $id ) { $id = (int) $id; - if ( empty( $id ) ) + if ( empty( $id ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } // Todo: Query args filter for wp_get_post_revisions $revisions = wp_get_post_revisions( $id ); @@ -118,15 +127,19 @@ public function get_posts( $filter = array(), $context = 'view', $type = 'post', // Validate post types and permissions $query['post_type'] = array(); + foreach ( (array) $type as $type_name ) { $post_type = get_post_type_object( $type_name ); - if ( ! ( (bool) $post_type ) || ! $post_type->show_in_json ) + + if ( ! ( (bool) $post_type ) || ! $post_type->show_in_json ) { return new WP_Error( 'json_invalid_post_type', sprintf( __( 'The post type "%s" is not valid' ), $type_name ), array( 'status' => 403 ) ); + } $query['post_type'][] = $post_type->name; } global $wp; + // Allow the same as normal WP $valid_vars = apply_filters('query_vars', $wp->public_query_vars); @@ -136,23 +149,23 @@ public function get_posts( $filter = array(), $context = 'view', $type = 'post', // To disable anyway, use `add_filter('json_private_query_vars', '__return_empty_array');` if ( current_user_can( $post_type->cap->edit_posts ) ) { - $private = apply_filters('json_private_query_vars', $wp->private_query_vars); - $valid_vars = array_merge($valid_vars, $private); + $private = apply_filters( 'json_private_query_vars', $wp->private_query_vars ); + $valid_vars = array_merge( $valid_vars, $private ); } // Define our own in addition to WP's normal vars - $json_valid = array('posts_per_page'); - $valid_vars = array_merge($valid_vars, $json_valid); + $json_valid = array( 'posts_per_page' ); + $valid_vars = array_merge( $valid_vars, $json_valid ); // Filter and flip for querying - $valid_vars = apply_filters('json_query_vars', $valid_vars); - $valid_vars = array_flip($valid_vars); + $valid_vars = apply_filters( 'json_query_vars', $valid_vars ); + $valid_vars = array_flip( $valid_vars ); // Exclude the post_type query var to avoid dodging the permission // check above - unset($valid_vars['post_type']); + unset( $valid_vars['post_type'] ); - foreach ($valid_vars as $var => $index) { + foreach ( $valid_vars as $var => $index ) { if ( isset( $filter[ $var ] ) ) { $query[ $var ] = apply_filters( 'json_query_var-' . $var, $filter[ $var ] ); } @@ -163,7 +176,7 @@ public function get_posts( $filter = array(), $context = 'view', $type = 'post', $post_query = new WP_Query(); $posts_list = $post_query->query( $query ); - $response = new WP_JSON_Response(); + $response = new WP_JSON_Response(); $response->query_navigation_headers( $post_query ); if ( ! $posts_list ) { @@ -180,8 +193,9 @@ public function get_posts( $filter = array(), $context = 'view', $type = 'post', $post = get_object_vars( $post ); // Do we have permission to read this post? - if ( ! $this->check_read_permission( $post ) ) + if ( ! $this->check_read_permission( $post ) ) { continue; + } $response->link_header( 'item', json_url( '/posts/' . $post['ID'] ), array( 'title' => $post['post_title'] ) ); $struct[] = $this->prepare_post( $post, $context ); @@ -282,6 +296,7 @@ function new_post( $data ) { $response = json_ensure_response( $this->get_post( $result ) ); $response->set_status( 201 ); $response->header( 'Location', json_url( '/posts/' . $result ) ); + return $response; } @@ -296,16 +311,19 @@ function new_post( $data ) { public function get_post( $id, $context = 'view' ) { $id = (int) $id; - if ( empty( $id ) ) + if ( empty( $id ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) + if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } - if ( ! $this->check_read_permission( $post ) ) + if ( ! $this->check_read_permission( $post ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); + } // Link headers (see RFC 5988) @@ -313,15 +331,18 @@ public function get_post( $id, $context = 'view' ) { $response->header( 'Last-Modified', mysql2date( 'D, d M Y H:i:s', $post['post_modified_gmt'] ) . 'GMT' ); $post = $this->prepare_post( $post, $context ); - if ( is_wp_error( $post ) ) + + if ( is_wp_error( $post ) ) { return $post; + } foreach ( $post['meta']['links'] as $rel => $url ) { $response->link_header( $rel, $url ); } - $response->link_header( 'alternate', get_permalink( $id ), array( 'type' => 'text/html' ) ); + $response->link_header( 'alternate', get_permalink( $id ), array( 'type' => 'text/html' ) ); $response->set_data( $post ); + return $response; } @@ -342,23 +363,27 @@ public function get_post( $id, $context = 'view' ) { function edit_post( $id, $data, $_headers = array() ) { $id = (int) $id; - if ( empty( $id ) ) + if ( empty( $id ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) + if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } if ( isset( $_headers['IF_UNMODIFIED_SINCE'] ) ) { // As mandated by RFC2616, we have to check all of RFC1123, RFC1036 // and C's asctime() format (and ignore invalid headers) $formats = array( DateTime::RFC1123, DateTime::RFC1036, 'D M j H:i:s Y' ); + foreach ( $formats as $format ) { $check = WP_JSON_DateTime::createFromFormat( $format, $_headers['IF_UNMODIFIED_SINCE'] ); - if ( $check !== false ) + if ( $check !== false ) { break; + } } // If the post has been modified since the date provided, return an error. @@ -387,27 +412,31 @@ function edit_post( $id, $data, $_headers = array() ) { public function delete_post( $id, $force = false ) { $id = (int) $id; - if ( empty( $id ) ) + if ( empty( $id ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) + if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } $post_type = get_post_type_object( $post['post_type'] ); - if ( ! current_user_can( $post_type->cap->delete_post, $id ) ) + + if ( ! current_user_can( $post_type->cap->delete_post, $id ) ) { return new WP_Error( 'json_user_cannot_delete_post', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => 401 ) ); + } $result = wp_delete_post( $id, $force ); - if ( ! $result ) + if ( ! $result ) { return new WP_Error( 'json_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) ); + } if ( $force ) { return array( 'message' => __( 'Permanently deleted post' ) ); - } - else { + } else { // TODO: return a HTTP 202 here instead return array( 'message' => __( 'Deleted post' ) ); } @@ -425,26 +454,29 @@ public function delete_post( $id, $force = false ) { public function delete_comment( $id, $comment, $force = false ) { $comment = (int) $comment; - if ( empty( $comment ) ) + if ( empty( $comment ) ) { return new WP_Error( 'json_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); + } $comment_array = get_comment( $comment, ARRAY_A ); - if ( empty( $comment_array ) ) + if ( empty( $comment_array ) ) { return new WP_Error( 'json_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); + } - if ( ! current_user_can( 'edit_comment', $comment_array['comment_ID'] ) ) + if ( ! current_user_can( 'edit_comment', $comment_array['comment_ID'] ) ) { return new WP_Error( 'json_user_cannot_delete_comment', __( 'Sorry, you are not allowed to delete this comment.' ), array( 'status' => 401 ) ); + } $result = wp_delete_comment( $comment_array['comment_ID'], $force ); - if ( ! $result ) + if ( ! $result ) { return new WP_Error( 'json_cannot_delete', __( 'The comment cannot be deleted.' ), array( 'status' => 500 ) ); + } if ( $force ) { return array( 'message' => __( 'Permanently deleted comment' ) ); - } - else { + } else { // TODO: return a HTTP 202 here instead return array( 'message' => __( 'Deleted comment' ) ); } @@ -471,9 +503,11 @@ public function get_comments( $id ) { } $struct = array(); + foreach ( $comments as $comment ) { $struct[] = $this->prepare_comment( $comment, array( 'comment', 'meta' ), 'collection' ); } + return $struct; } @@ -485,11 +519,13 @@ public function get_comments( $id ) { */ public function get_comment( $comment ) { $comment = get_comment( $comment ); + if ( empty( $comment ) ) { return new WP_Error( 'json_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); } $data = $this->prepare_comment( $comment ); + return $data; } @@ -503,10 +539,12 @@ public function get_post_types() { $data = get_post_types( array(), 'objects' ); $types = array(); + foreach ($data as $name => $type) { $type = $this->get_post_type( $type, true ); - if ( is_wp_error( $type ) ) + if ( is_wp_error( $type ) ) { continue; + } $types[ $name ] = $type; } @@ -522,36 +560,39 @@ public function get_post_types() { * @return array Post type data */ public function get_post_type( $type, $_in_collection = false ) { - if ( ! is_object( $type ) ) - $type = get_post_type_object($type); + if ( ! is_object( $type ) ) { + $type = get_post_type_object( $type ); + } if ( $type->show_in_json === false ) { return new WP_Error( 'json_cannot_read_type', __( 'Cannot view post type' ), array( 'status' => 403 ) ); } $data = array( - 'name' => $type->label, - 'slug' => $type->name, - 'description' => $type->description, - 'labels' => $type->labels, - 'queryable' => $type->publicly_queryable, - 'searchable' => ! $type->exclude_from_search, + 'name' => $type->label, + 'slug' => $type->name, + 'description' => $type->description, + 'labels' => $type->labels, + 'queryable' => $type->publicly_queryable, + 'searchable' => ! $type->exclude_from_search, 'hierarchical' => $type->hierarchical, - 'meta' => array( + 'meta' => array( 'links' => array() ), ); - if ( $_in_collection ) + if ( $_in_collection ) { $data['meta']['links']['self'] = json_url( '/posts/types/' . $type->name ); - else + } else { $data['meta']['links']['collection'] = json_url( '/posts/types' ); + } if ( $type->publicly_queryable ) { - if ($type->name === 'post') + if ( $type->name === 'post' ) { $data['meta']['links']['archives'] = json_url( '/posts' ); - else + } else { $data['meta']['links']['archives'] = json_url( add_query_arg( 'type', $type->name, '/posts' ) ); + } } return apply_filters( 'json_post_type_data', $data, $type ); @@ -566,27 +607,30 @@ public function get_post_statuses() { $statuses = get_post_stati(array(), 'objects'); $data = array(); + foreach ($statuses as $status) { - if ( $status->internal === true || ! $status->show_in_admin_status_list ) + if ( $status->internal === true || ! $status->show_in_admin_status_list ) { continue; + } $data[ $status->name ] = array( - 'name' => $status->label, - 'slug' => $status->name, - 'public' => $status->public, - 'protected' => $status->protected, - 'private' => $status->private, - 'queryable' => $status->publicly_queryable, + 'name' => $status->label, + 'slug' => $status->name, + 'public' => $status->public, + 'protected' => $status->protected, + 'private' => $status->private, + 'queryable' => $status->publicly_queryable, 'show_in_list' => $status->show_in_admin_all_list, - 'meta' => array( + 'meta' => array( 'links' => array() ), ); if ( $status->publicly_queryable ) { - if ($status->name === 'publish') + if ( $status->name === 'publish' ) { $data[ $status->name ]['meta']['links']['archives'] = json_url( '/posts' ); - else + } else { $data[ $status->name ]['meta']['links']['archives'] = json_url( add_query_arg( 'status', $status->name, '/posts' ) ); + } } } @@ -604,25 +648,26 @@ public function get_post_statuses() { */ protected function prepare_post( $post, $context = 'view' ) { // holds the data for this post. built up based on $fields - $_post = array( - 'ID' => (int) $post['ID'], - ); + $_post = array( 'ID' => (int) $post['ID'] ); $post_type = get_post_type_object( $post['post_type'] ); - if ( ! $this->check_read_permission( $post ) ) + + if ( ! $this->check_read_permission( $post ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); + } // prepare common post fields $post_fields = array( - 'title' => get_the_title( $post['ID'] ), // $post['post_title'], - 'status' => $post['post_status'], - 'type' => $post['post_type'], - 'author' => (int) $post['post_author'], - 'content' => apply_filters( 'the_content', $post['post_content'] ), - 'parent' => (int) $post['post_parent'], - #'post_mime_type' => $post['post_mime_type'], - 'link' => get_permalink( $post['ID'] ), + 'title' => get_the_title( $post['ID'] ), // $post['post_title'], + 'status' => $post['post_status'], + 'type' => $post['post_type'], + 'author' => (int) $post['post_author'], + 'content' => apply_filters( 'the_content', $post['post_content'] ), + 'parent' => (int) $post['post_parent'], + #'post_mime_type' => $post['post_mime_type'], + 'link' => get_permalink( $post['ID'] ), ); + $post_fields_extended = array( 'slug' => $post['post_name'], 'guid' => apply_filters( 'get_the_guid', $post['guid'] ), @@ -632,6 +677,7 @@ protected function prepare_post( $post, $context = 'view' ) { 'ping_status' => $post['ping_status'], 'sticky' => ( $post['post_type'] === 'post' && is_sticky( $post['ID'] ) ), ); + $post_fields_raw = array( 'title_raw' => $post['post_title'], 'content_raw' => $post['post_content'], @@ -660,13 +706,16 @@ protected function prepare_post( $post, $context = 'view' ) { } // Consider future posts as published - if ( $post_fields['status'] === 'future' ) + if ( $post_fields['status'] === 'future' ) { $post_fields['status'] = 'publish'; + } // Fill in blank post format $post_fields['format'] = get_post_format( $post['ID'] ); - if ( empty( $post_fields['format'] ) ) + + if ( empty( $post_fields['format'] ) ) { $post_fields['format'] = 'standard'; + } if ( ( 'view' === $context || 'view-revision' == $context ) && 0 !== $post['post_parent'] ) { // Avoid nesting too deeply @@ -702,9 +751,9 @@ protected function prepare_post( $post, $context = 'view' ) { // Entity meta $links = array( - 'self' => json_url( '/posts/' . $post['ID'] ), - 'author' => json_url( '/users/' . $post['post_author'] ), - 'collection' => json_url( '/posts' ), + 'self' => json_url( '/posts/' . $post['ID'] ), + 'author' => json_url( '/users/' . $post['post_author'] ), + 'collection' => json_url( '/posts' ), ); if ( 'view-revision' != $context ) { @@ -714,8 +763,9 @@ protected function prepare_post( $post, $context = 'view' ) { $_post['meta'] = array( 'links' => $links ); - if ( ! empty( $post['post_parent'] ) ) + if ( ! empty( $post['post_parent'] ) ) { $_post['meta']['links']['up'] = json_url( '/posts/' . (int) $post['post_parent'] ); + } return apply_filters( 'json_prepare_post', $_post, $post, $context ); } @@ -731,9 +781,11 @@ protected function prepare_excerpt( $excerpt ) { } $excerpt = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $excerpt ) ); + if ( empty( $excerpt ) ) { return null; } + return $excerpt; } @@ -751,6 +803,7 @@ public function get_all_meta( $id ) { } $post = get_post( $id, ARRAY_A ); + if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -764,8 +817,10 @@ public function get_all_meta( $id ) { $results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_key, meta_value FROM $table WHERE post_id = %d", $id ) ); $meta = array(); + foreach ( $results as $row ) { $value = $this->prepare_meta( $id, $row, true ); + if ( is_wp_error( $value ) ) { continue; } @@ -791,6 +846,7 @@ public function get_meta( $id, $mid ) { } $post = get_post( $id, ARRAY_A ); + if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -800,6 +856,7 @@ public function get_meta( $id, $mid ) { } $meta = get_metadata_by_mid( 'post', $mid ); + if ( empty( $meta ) ) { return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); } @@ -840,8 +897,8 @@ protected function prepare_meta( $post, $data, $is_raw = false ) { } $meta = array( - 'ID' => (int) $ID, - 'key' => $key, + 'ID' => (int) $ID, + 'key' => $key, 'value' => $value, ); @@ -873,8 +930,7 @@ protected function handle_post_meta_action( $post_id, $data ) { if ( empty( $meta_array['ID'] ) ) { // Creation $result = $this->add_meta( $post_id, $meta_array ); - } - else { + } else { // Update $result = $this->update_meta( $post_id, $meta_array['ID'], $meta_array ); } @@ -898,7 +954,7 @@ protected function handle_post_meta_action( $post_id, $data ) { * @return bool|WP_Error */ public function update_meta( $id, $mid, $data ) { - $id = (int) $id; + $id = (int) $id; $mid = (int) $mid; if ( empty( $id ) ) { @@ -906,6 +962,7 @@ public function update_meta( $id, $mid, $data ) { } $post = get_post( $id, ARRAY_A ); + if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -915,6 +972,7 @@ public function update_meta( $id, $mid, $data ) { } $current = get_metadata_by_mid( 'post', $mid ); + if ( empty( $current ) ) { return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); } @@ -926,6 +984,7 @@ public function update_meta( $id, $mid, $data ) { if ( ! array_key_exists( 'key', $data ) ) { $data['key'] = $current->meta_key; } + if ( ! array_key_exists( 'value', $data ) ) { $data['value'] = $current->meta_value; } @@ -938,12 +997,15 @@ public function update_meta( $id, $mid, $data ) { if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); } + if ( ! $this->is_valid_meta_data( $data['value'] ) ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } + if ( is_protected_meta( $current->meta_key ) ) { return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $current->meta_key ), array( 'status' => 403 ) ); } + if ( is_protected_meta( $data['key'] ) ) { return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $data['key'] ), array( 'status' => 403 ) ); } @@ -954,8 +1016,9 @@ public function update_meta( $id, $mid, $data ) { return $this->get_meta( $id, $mid ); } - $key = wp_slash( $data['key'] ); + $key = wp_slash( $data['key'] ); $value = wp_slash( $data['value'] ); + if ( ! update_metadata_by_mid( 'post', $mid, $value, $key ) ) { return new WP_Error( 'json_meta_could_not_update', __( 'Could not update post meta.' ), array( 'status' => 500 ) ); } @@ -998,6 +1061,7 @@ public function add_meta( $id, $data ) { } $post = get_post( $id, ARRAY_A ); + if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -1021,24 +1085,29 @@ public function add_meta( $id, $data ) { // for now let's not allow updating of arrays, objects or serialized values. return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } + if ( is_protected_meta( $data['key'] ) ) { return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $data['key'] ), array( 'status' => 403 ) ); } $meta_key = wp_slash( $data['key'] ); - $value = wp_slash( $data['value'] ); + $value = wp_slash( $data['value'] ); $result = add_post_meta( $id, $meta_key, $value ); + if ( ! $result ) { return new WP_Error( 'json_meta_could_not_add', __( 'Could not add post meta.' ), array( 'status' => 400 ) ); } $response = json_ensure_response( $this->get_meta( $id, $result ) ); + if ( is_wp_error( $response ) ) { return $response; } + $response->set_status( 201 ); $response->header( 'Location', json_url( '/posts/' . $id . '/meta/' . $result ) ); + return $response; } @@ -1057,6 +1126,7 @@ public function delete_meta( $id, $mid ) { } $post = get_post( $id, ARRAY_A ); + if ( empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -1066,6 +1136,7 @@ public function delete_meta( $id, $mid ) { } $current = get_metadata_by_mid( 'post', $mid ); + if ( empty( $current ) ) { return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); } @@ -1078,6 +1149,7 @@ public function delete_meta( $id, $mid ) { if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { return new WP_Error( 'json_post_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); } + if ( is_protected_meta( $current->meta_key ) ) { return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $current->meta_key ), array( 'status' => 403 ) ); } @@ -1099,79 +1171,93 @@ public function delete_meta( $id, $mid ) { * @param array $content_struct Post data to insert. */ protected function insert_post( $data ) { - $post = array(); + $post = array(); $update = ! empty( $data['ID'] ); if ( $update ) { $current_post = get_post( absint( $data['ID'] ) ); - if ( ! $current_post ) + + if ( ! $current_post ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 400 ) ); + } + $post['ID'] = absint( $data['ID'] ); - } - else { + } else { // Defaults - $post['post_author'] = 0; + $post['post_author'] = 0; $post['post_password'] = ''; - $post['post_excerpt'] = ''; - $post['post_content'] = ''; - $post['post_title'] = ''; + $post['post_excerpt'] = ''; + $post['post_content'] = ''; + $post['post_title'] = ''; } // Post type if ( ! empty( $data['type'] ) ) { // Changing post type $post_type = get_post_type_object( $data['type'] ); - if ( ! $post_type ) + + if ( ! $post_type ) { return new WP_Error( 'json_invalid_post_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); + } $post['post_type'] = $data['type']; - } - elseif ( $update ) { + } elseif ( $update ) { // Updating post, use existing post type $current_post = get_post( $data['ID'] ); - if ( ! $current_post ) + + if ( ! $current_post ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 400 ) ); + } $post_type = get_post_type_object( $current_post->post_type ); - } - else { + } else { // Creating new post, use default type $post['post_type'] = apply_filters( 'json_insert_default_post_type', 'post' ); $post_type = get_post_type_object( $post['post_type'] ); - if ( ! $post_type ) + + if ( ! $post_type ) { return new WP_Error( 'json_invalid_post_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); + } } // Permissions check if ( $update ) { - if ( ! current_user_can( $post_type->cap->edit_post, $data['ID'] ) ) + if ( ! current_user_can( $post_type->cap->edit_post, $data['ID'] ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => 401 ) ); - if ( $post_type->name != get_post_type( $data['ID'] ) ) + } + + if ( $post_type->name != get_post_type( $data['ID'] ) ) { return new WP_Error( 'json_cannot_change_post_type', __( 'The post type may not be changed.' ), array( 'status' => 400 ) ); + } } else { - if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) + if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) { return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to post on this site.' ), array( 'status' => 400 ) ); + } } // Post status if ( ! empty( $data['status'] ) ) { $post['post_status'] = $data['status']; + switch ( $post['post_status'] ) { case 'draft': case 'pending': break; case 'private': - if ( ! current_user_can( $post_type->cap->publish_posts ) ) + if ( ! current_user_can( $post_type->cap->publish_posts ) ) { return new WP_Error( 'json_cannot_create_private', __( 'Sorry, you are not allowed to create private posts in this post type' ), array( 'status' => 403 ) ); + } break; case 'publish': case 'future': - if ( ! current_user_can( $post_type->cap->publish_posts ) ) + if ( ! current_user_can( $post_type->cap->publish_posts ) ) { return new WP_Error( 'json_cannot_publish', __( 'Sorry, you are not allowed to publish posts in this post type' ), array( 'status' => 403 ) ); + } break; default: - if ( ! get_post_status_object( $post['post_status'] ) ) + if ( ! get_post_status_object( $post['post_status'] ) ) { $post['post_status'] = 'draft'; + } break; } } @@ -1184,12 +1270,13 @@ protected function insert_post( $data ) { // Post date if ( ! empty( $data['date'] ) ) { $date_data = $this->server->get_date_with_gmt( $data['date'] ); + if ( ! empty( $date_data ) ) { list( $post['post_date'], $post['post_date_gmt'] ) = $date_data; } - } - elseif ( ! empty( $data['date_gmt'] ) ) { + } elseif ( ! empty( $data['date_gmt'] ) ) { $date_data = $this->server->get_date_with_gmt( $data['date_gmt'], true ); + if ( ! empty( $date_data ) ) { list( $post['post_date'], $post['post_date_gmt'] ) = $date_data; } @@ -1198,12 +1285,13 @@ protected function insert_post( $data ) { // Post modified if ( ! empty( $data['modified'] ) ) { $date_data = $this->server->get_date_with_gmt( $data['modified'] ); + if ( ! empty( $date_data ) ) { list( $post['post_modified'], $post['post_modified_gmt'] ) = $date_data; } - } - elseif ( ! empty( $data['modified_gmt'] ) ) { + } elseif ( ! empty( $data['modified_gmt'] ) ) { $date_data = $this->server->get_date_with_gmt( $data['modified_gmt'], true ); + if ( ! empty( $date_data ) ) { list( $post['post_modified'], $post['post_modified_gmt'] ) = $date_data; } @@ -1222,20 +1310,21 @@ protected function insert_post( $data ) { return new WP_Error( 'json_invalid_author', __( 'Invalid author object.' ), array( 'status' => 400 ) ); } $data['author'] = absint( $data['author']->ID ); - } - else { + } else { $data['author'] = absint( $data['author'] ); } // Only check edit others' posts if we are another user if ( $data['author'] !== get_current_user_id() ) { - if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) + if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) { return new WP_Error( 'json_cannot_edit_others', __( 'You are not allowed to edit posts as this user.' ), array( 'status' => 401 ) ); + } $author = get_userdata( $data['author'] ); - if ( ! $author ) + if ( ! $author ) { return new WP_Error( 'json_invalid_author', __( 'Invalid author ID.' ), array( 'status' => 400 ) ); + } } $post['post_author'] = $data['author']; @@ -1244,14 +1333,17 @@ protected function insert_post( $data ) { // Post password if ( ! empty( $data['password'] ) ) { $post['post_password'] = $data['password']; - if ( ! current_user_can( $post_type->cap->publish_posts ) ) + + if ( ! current_user_can( $post_type->cap->publish_posts ) ) { return new WP_Error( 'json_cannot_create_passworded', __( 'Sorry, you are not allowed to create password protected posts in this post type' ), array( 'status' => 401 ) ); + } } // Content and excerpt if ( ! empty( $data['content_raw'] ) ) { $post['post_content'] = $data['content_raw']; } + if ( ! empty( $data['excerpt_raw'] ) ) { $post['post_excerpt'] = $data['excerpt_raw']; } @@ -1280,6 +1372,7 @@ protected function insert_post( $data ) { // Post format if ( ! empty( $data['post_format'] ) ) { $formats = get_post_format_slugs(); + if ( ! in_array( $data['post_format'], $formats ) ) { return new WP_Error( 'json_invalid_post_format', __( 'Invalid post format.' ), array( 'status' => 400 ) ); } @@ -1288,6 +1381,7 @@ protected function insert_post( $data ) { // Pre-insert hook $can_insert = apply_filters( 'json_pre_insert_post', true, $post, $data, $update ); + if ( is_wp_error( $can_insert ) ) { return $can_insert; } @@ -1308,6 +1402,7 @@ protected function insert_post( $data ) { // Post meta if ( ! empty( $data['post_meta'] ) ) { $result = $this->handle_post_meta_action( $post_ID, $data['post_meta'] ); + if ( is_wp_error( $result ) ) { return $result; } @@ -1315,10 +1410,11 @@ protected function insert_post( $data ) { // Sticky if ( isset( $post['sticky'] ) ) { - if ( $post['sticky'] ) + if ( $post['sticky'] ) { stick_post( $data['ID'] ); - else + } else { unstick_post( $data['ID'] ); + } } do_action( 'json_insert_post', $post, $data, $update ); @@ -1336,7 +1432,7 @@ protected function insert_post( $data ) { */ protected function prepare_comment( $comment, $requested_fields = array( 'comment', 'meta' ), $context = 'single' ) { $fields = array( - 'ID' => (int) $comment->comment_ID, + 'ID' => (int) $comment->comment_ID, 'post' => (int) $comment->comment_post_ID, ); @@ -1362,10 +1458,12 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen case 'trash': default: $fields['status'] = $comment->comment_approved; + break; } // Type $fields['type'] = apply_filters( 'get_comment_type', $comment->comment_type ); + if ( empty( $fields['type'] ) ) { $fields['type'] = 'comment'; } @@ -1379,9 +1477,12 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen // Parent if ( ( 'single' === $context || 'single-parent' === $context ) && (int) $comment->comment_parent ) { $parent_fields = array( 'meta' ); - if ( $context === 'single' ) + + if ( $context === 'single' ) { $parent_fields[] = 'comment'; + } $parent = get_comment( $post['post_parent'] ); + $fields['parent'] = $this->prepare_comment( $parent, $parent_fields, 'single-parent' ); } @@ -1391,12 +1492,11 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen // Author if ( (int) $comment->user_id !== 0 ) { $fields['author'] = (int) $comment->user_id; - } - else { + } else { $fields['author'] = array( - 'ID' => 0, - 'name' => $comment->comment_author, - 'URL' => $comment->comment_author_url, + 'ID' => 0, + 'name' => $comment->comment_author, + 'URL' => $comment->comment_author_url, 'avatar' => $this->server->get_avatar_url( $comment->comment_author_email ), ); } @@ -1404,9 +1504,9 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen // Date $timezone = $this->server->get_timezone(); - $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $comment->comment_date, $timezone ); - $fields['date'] = $date->format( 'c' ); - $fields['date_tz'] = $date->format( 'e' ); + $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $comment->comment_date, $timezone ); + $fields['date'] = $date->format( 'c' ); + $fields['date_tz'] = $date->format( 'e' ); $fields['date_gmt'] = date( 'c', strtotime( $comment->comment_date_gmt ) ); // Meta @@ -1415,20 +1515,25 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen 'up' => json_url( sprintf( '/posts/%d', (int) $comment->comment_post_ID ) ) ), ); + if ( 0 !== (int) $comment->comment_parent ) { $meta['links']['in-reply-to'] = json_url( sprintf( '/posts/%d/comments/%d', (int) $comment->comment_post_ID, (int) $comment->comment_parent ) ); } + if ( 'single' !== $context ) { $meta['links']['self'] = json_url( sprintf( '/posts/%d/comments/%d', (int) $comment->comment_post_ID, (int) $comment->comment_ID ) ); } // Remove unneeded fields $data = array(); - if ( in_array( 'comment', $requested_fields ) ) + + if ( in_array( 'comment', $requested_fields ) ) { $data = array_merge( $data, $fields ); + } - if ( in_array( 'meta', $requested_fields ) ) + if ( in_array( 'meta', $requested_fields ) ) { $data['meta'] = $meta; + } return apply_filters( 'json_prepare_comment', $data, $comment, $context ); } From bb597ab3b3ae9b28a482e1b229c9fadbc175aee9 Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 27 May 2014 11:32:54 -0600 Subject: [PATCH 232/515] Coding standards lib/class-wp-json-response.php. --- lib/class-wp-json-response.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/class-wp-json-response.php b/lib/class-wp-json-response.php index f3e9f7182c..2758a66417 100644 --- a/lib/class-wp-json-response.php +++ b/lib/class-wp-json-response.php @@ -28,7 +28,7 @@ public function get_headers() { * * @param array $headers Map of header name to header value */ - public function set_headers($headers) { + public function set_headers( $headers ) { $this->headers = $headers; } @@ -39,11 +39,10 @@ public function set_headers($headers) { * @param string $value Header value * @param boolean $replace Replace an existing header of the same name? */ - public function header($key, $value, $replace = true) { + public function header( $key, $value, $replace = true ) { if ( $replace || ! isset( $this->headers[ $key ] ) ) { $this->headers[ $key ] = $value; - } - else { + } else { $this->headers[ $key ] .= ', ' . $value; } } @@ -62,9 +61,11 @@ public function header($key, $value, $replace = true) { */ public function link_header( $rel, $link, $other = array() ) { $header = '<' . $link . '>; rel="' . $rel . '"'; + foreach ( $other as $key => $value ) { - if ( 'title' == $key ) + if ( 'title' == $key ) { $value = '"' . $value . '"'; + } $header .= '; ' . $key . '=' . $value; } return $this->header( 'Link', $header, false ); @@ -77,10 +78,11 @@ public function link_header( $rel, $link, $other = array() ) { */ public function query_navigation_headers( $query ) { $max_page = $query->max_num_pages; - $paged = $query->get('paged'); + $paged = $query->get('paged'); - if ( !$paged ) + if ( ! $paged ) { $paged = 1; + } $nextpage = intval($paged) + 1; From 784f6dcaba01e676d4ac230dc21eb3bf897651db Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 27 May 2014 11:34:02 -0600 Subject: [PATCH 233/515] Coding standards lib/class-wp-json-responsehandler.php. --- lib/class-wp-json-responsehandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-responsehandler.php b/lib/class-wp-json-responsehandler.php index a7cace65e1..fb68477d6b 100644 --- a/lib/class-wp-json-responsehandler.php +++ b/lib/class-wp-json-responsehandler.php @@ -15,4 +15,4 @@ public function serve_request( $path = null ); * @return string */ public function get_raw_data(); -} \ No newline at end of file +} From a4f0eafd91bf0081ab490f059611edece39d9dc4 Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 27 May 2014 11:37:21 -0600 Subject: [PATCH 234/515] Coding standards lib/class-wp-json-taxonomies.php. --- lib/class-wp-json-taxonomies.php | 56 +++++++++++++++++++------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 51f7fc9537..8a7dd64b13 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -13,13 +13,13 @@ public function register_routes( $routes ) { array( array( $this, 'get_taxonomies' ), WP_JSON_Server::READABLE ), ), '/posts/types/(?P\w+)/taxonomies/(?P\w+)' => array( - array( array( $this, 'get_taxonomy' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_taxonomy' ), WP_JSON_Server::READABLE ), ), '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms' => array( - array( array( $this, 'get_terms' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_terms' ), WP_JSON_Server::READABLE ), ), '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms/(?P\w+)' => array( - array( array( $this, 'get_term' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_term' ), WP_JSON_Server::READABLE ), ), ); return array_merge( $routes, $tax_routes ); @@ -35,10 +35,12 @@ public function get_taxonomies( $type ) { $taxonomies = get_object_taxonomies( $type, 'objects' ); $data = array(); + foreach ($taxonomies as $tax_type => $value) { $tax = $this->prepare_taxonomy( $value, $type, true ); - if ( is_wp_error( $tax ) ) + if ( is_wp_error( $tax ) ) { continue; + } $data[] = $tax; } @@ -54,8 +56,10 @@ public function get_taxonomies( $type ) { */ public function get_taxonomy( $type, $taxonomy ) { $tax = get_taxonomy( $taxonomy ); - if ( empty( $tax ) ) + + if ( empty( $tax ) ) { return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); + } return $this->prepare_taxonomy( $tax, $type ); } @@ -68,19 +72,20 @@ public function get_taxonomy( $type, $taxonomy ) { * @return array Taxonomy data */ protected function prepare_taxonomy( $taxonomy, $type, $_in_collection = false ) { - if ( $taxonomy->public === false ) + if ( $taxonomy->public === false ) { return new WP_Error( 'json_cannot_read_taxonomy', __( 'Cannot view taxonomy' ), array( 'status' => 403 ) ); + } $base_url = '/posts/types/' . $type . '/taxonomies/' . $taxonomy->name; $data = array( - 'name' => $taxonomy->label, - 'slug' => $taxonomy->name, - 'labels' => $taxonomy->labels, - 'types' => array(), - 'show_cloud' => $taxonomy->show_tagcloud, + 'name' => $taxonomy->label, + 'slug' => $taxonomy->name, + 'labels' => $taxonomy->labels, + 'types' => array(), + 'show_cloud' => $taxonomy->show_tagcloud, 'hierarchical' => $taxonomy->hierarchical, - 'meta' => array( + 'meta' => array( 'links' => array( 'archives' => json_url( $base_url . '/terms' ) ) @@ -89,8 +94,7 @@ protected function prepare_taxonomy( $taxonomy, $type, $_in_collection = false ) if ( $_in_collection ) { $data['meta']['links']['self'] = json_url( $base_url ); - } - else { + } else { $data['meta']['links']['collection'] = json_url( $base_url ); } @@ -118,18 +122,23 @@ public function add_taxonomy_data( $data, $type ) { * @return array Term collection */ public function get_terms( $type, $taxonomy ) { - if ( ! taxonomy_exists( $taxonomy ) ) + if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); + } $args = array( 'hide_empty' => false, ); + $terms = get_terms( $taxonomy, $args ); - if ( is_wp_error( $terms ) ) + + if ( is_wp_error( $terms ) ) { return $terms; + } $data = array(); - foreach ($terms as $term) { + + foreach ( $terms as $term ) { $data[] = $this->prepare_term( $term, $type ); } return $data; @@ -145,12 +154,15 @@ public function get_terms( $type, $taxonomy ) { * @return array Term entity */ public function get_term( $type, $taxonomy, $term, $context = 'view' ) { - if ( ! taxonomy_exists( $taxonomy ) ) + if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); + } $data = get_term( $term, $taxonomy ); - if ( empty( $data ) ) + + if ( empty( $data ) ) { return new WP_Error( 'json_taxonomy_invalid_term', __( 'Invalid term ID.' ), array( 'status' => 404 ) ); + } return $this->prepare_term( $data, $type, $context ); } @@ -167,6 +179,7 @@ public function add_term_data( $data, $post, $context ) { $post_type_taxonomies = get_object_taxonomies( $post['post_type'] ); $terms = wp_get_object_terms( $post['ID'], $post_type_taxonomies ); $data['terms'] = array(); + foreach ( $terms as $term ) { $data['terms'][ $term->taxonomy ][] = $this->prepare_term( $term, $post['post_type'] ); } @@ -193,15 +206,14 @@ protected function prepare_term( $term, $type, $context = 'view' ) { 'meta' => array( 'links' => array( 'collection' => json_url( $base_url ), - 'self' => json_url( $base_url . '/' . $term->term_id ), + 'self' => json_url( $base_url . '/' . $term->term_id ), ), ), ); if ( ! empty( $data['parent'] ) && $context === 'view' ) { $data['parent'] = $this->get_term( $type, $term->taxonomy, $data['parent'], 'view-parent' ); - } - elseif ( empty( $data['parent'] ) ) { + } elseif ( empty( $data['parent'] ) ) { $data['parent'] = null; } From 2a4ff0512880de767a60f5fa66fc5c69dcaff777 Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 27 May 2014 11:42:11 -0600 Subject: [PATCH 235/515] Coding standards lib/class-wp-json-users.php. --- lib/class-wp-json-users.php | 66 +++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 4110a05bbc..85d7d019d8 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -27,13 +27,13 @@ public function register_routes( $routes ) { $user_routes = array( // User endpoints '/users' => array( - array( array( $this, 'get_users' ), WP_JSON_Server::READABLE ), - array( array( $this, 'new_user' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), + array( array( $this, 'get_users' ), WP_JSON_Server::READABLE ), + array( array( $this, 'new_user' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), ), '/users/(?P\d+)' => array( - array( array( $this, 'get_user' ), WP_JSON_Server::READABLE ), - array( array( $this, 'edit_user' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), - array( array( $this, 'delete_user' ), WP_JSON_Server::DELETABLE ), + array( array( $this, 'get_user' ), WP_JSON_Server::READABLE ), + array( array( $this, 'edit_user' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), + array( array( $this, 'delete_user' ), WP_JSON_Server::DELETABLE ), ), // /users/me is an alias, and simply redirects to /users/ '/users/me' => array( @@ -58,22 +58,25 @@ public function get_users( $filter = array(), $context = 'view', $page = 1 ) { $args = array( 'orderby' => 'user_login', - 'order' => 'ASC' + 'order' => 'ASC' ); $args = array_merge( $args, $filter ); + $args = apply_filters( 'json_user_query', $args, $filter, $context, $page ); // Pagination $args['number'] = empty( $args['number'] ) ? 10 : absint( $args['number'] ); - $page = absint( $page ); + $page = absint( $page ); $args['offset'] = ( $page - 1 ) * $args['number']; $user_query = new WP_User_Query( $args ); + if ( empty( $user_query->results ) ) { return array(); } $struct = array(); + foreach ( $user_query->results as $user ) { $struct[] = $this->prepare_user( $user, $context ); } @@ -89,11 +92,13 @@ public function get_users( $filter = array(), $context = 'view', $page = 1 ) { */ public function get_current_user( $context = 'view' ) { $current_user_id = get_current_user_id(); + if ( empty( $current_user_id ) ) { return new WP_Error( 'json_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) ); } $response = $this->get_user( $current_user_id, $context ); + if ( is_wp_error( $response ) ) { return $response; } @@ -119,6 +124,7 @@ public function get_current_user( $context = 'view' ) { */ public function get_user( $id, $context = 'view' ) { $current_user_id = get_current_user_id(); + if ( $current_user_id !== $id && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'json_user_cannot_list', __( 'Sorry, you are not allowed to view this user.' ), array( 'status' => 403 ) ); } @@ -142,23 +148,23 @@ public function get_user( $id, $context = 'view' ) { */ protected function prepare_user( $user, $context = 'view' ) { $user_fields = array( - 'ID' => $user->ID, - 'username' => $user->user_login, - 'name' => $user->display_name, - 'first_name' => $user->first_name, - 'last_name' => $user->last_name, - 'nickname' => $user->nickname, - 'slug' => $user->user_nicename, - 'URL' => $user->user_url, - 'avatar' => $this->server->get_avatar_url( $user->user_email ), + 'ID' => $user->ID, + 'username' => $user->user_login, + 'name' => $user->display_name, + 'first_name' => $user->first_name, + 'last_name' => $user->last_name, + 'nickname' => $user->nickname, + 'slug' => $user->user_nicename, + 'URL' => $user->user_url, + 'avatar' => $this->server->get_avatar_url( $user->user_email ), 'description' => $user->description, - 'email' => $user->user_email, + 'email' => $user->user_email, ); $user_fields['registered'] = date( 'c', strtotime( $user->user_registered ) ); if ( $context === 'view' || $context === 'edit' ) { - $user_fields['roles'] = $user->roles; + $user_fields['roles'] = $user->roles; $user_fields['capabilities'] = $user->allcaps; } @@ -174,6 +180,7 @@ protected function prepare_user( $user, $context = 'view' ) { 'archives' => json_url( '/users/' . $user->ID . '/posts' ), ), ); + return apply_filters( 'json_prepare_user', $user_fields, $user, $context ); } @@ -218,6 +225,7 @@ public function add_comment_author_data( $data, $comment, $context ) { protected function insert_user( $data ) { if ( ! empty( $data['ID'] ) ) { $user = get_userdata( $data['ID'] ); + if ( ! $user ) { return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); } @@ -227,6 +235,7 @@ protected function insert_user( $data ) { } $required = array( 'username', 'password', 'email' ); + foreach ( $required as $arg ) { if ( empty( $data[ $arg ] ) ) { return new WP_Error( 'json_missing_callback_param', sprintf( __( 'Missing parameter %s' ), $arg ), array( 'status' => 400 ) ); @@ -234,8 +243,7 @@ protected function insert_user( $data ) { } $update = true; - } - else { + } else { $user = new WP_User(); // Workaround for https://core.trac.wordpress.org/ticket/28019 @@ -252,6 +260,7 @@ protected function insert_user( $data ) { if ( isset( $data['username'] ) ) { $user->user_login = $data['username']; } + if ( isset( $data['password'] ) ) { $user->user_pass = $data['password']; } @@ -260,15 +269,19 @@ protected function insert_user( $data ) { if ( isset( $data['name'] ) ) { $user->display_name = $data['name']; } + if ( isset( $data['first_name'] ) ) { $user->first_name = $data['first_name']; } + if ( isset( $data['last_name'] ) ) { $user->last_name = $data['last_name']; } + if ( isset( $data['nickname'] ) ) { $user->nickname = $data['nickname']; } + if ( ! empty( $data['slug'] ) ) { $user->user_nicename = $data['slug']; } @@ -276,6 +289,7 @@ protected function insert_user( $data ) { // URL if ( ! empty( $data['URL'] ) ) { $escaped = esc_url_raw( $user->user_url ); + if ( $escaped !== $user->user_url ) { return new WP_Error( 'json_invalid_url', __( 'Invalid user URL.' ), array( 'status' => 400 ) ); } @@ -295,11 +309,13 @@ protected function insert_user( $data ) { // Pre-flight check $user = apply_filters( 'json_pre_insert_user', $user, $data ); + if ( is_wp_error( $user ) ) { return $user; } $user_id = $update ? wp_update_user( $user ) : wp_insert_user( $user ); + if ( is_wp_error( $user_id ) ) { return $user_id; } @@ -343,6 +359,7 @@ public function edit_user( $id, $data, $_headers = array() ) { // Update attributes of the user from $data $retval = $this->insert_user( $data ); + if ( is_wp_error( $retval ) ) { return $retval; } @@ -366,17 +383,20 @@ public function new_user( $data ) { } $user_id = $this->insert_user( $data ); + if ( is_wp_error( $user_id ) ) { return $user_id; } $response = $this->get_user( $user_id ); + if ( ! $response instanceof WP_JSON_ResponseInterface ) { $response = new WP_JSON_Response( $response ); } $response->set_status( 201 ); $response->header( 'Location', json_url( '/users/' . $user_id ) ); + return $response; } @@ -412,8 +432,7 @@ public function delete_user( $id, $force = false, $reassign = null ) { if ( empty( $reassign ) || $reassign === $id || ! get_userdata( $reassign ) ) { return new WP_Error( 'json_user_invalid_reassign', __( 'Invalid user ID.' ), array( 'status' => 400 ) ); } - } - else { + } else { $reassign = null; } @@ -421,8 +440,7 @@ public function delete_user( $id, $force = false, $reassign = null ) { if ( ! $result ) { return new WP_Error( 'json_cannot_delete', __( 'The user cannot be deleted.' ), array( 'status' => 500 ) ); - } - else { + } else { return array( 'message' => __( 'Deleted user' ) ); } } From 936bd44eb3c93b1f8e3c2632e536ec2fe2a4011a Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 27 May 2014 11:43:40 -0600 Subject: [PATCH 236/515] Coding standards lib/wp-json.php. --- lib/wp-json.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/wp-json.php b/lib/wp-json.php index 1e6b467009..6c0b1d8e4f 100644 --- a/lib/wp-json.php +++ b/lib/wp-json.php @@ -11,25 +11,26 @@ * @var bool * @todo Remove me in favour of JSON_REQUEST */ -define('XMLRPC_REQUEST', true); +define( 'XMLRPC_REQUEST', true ); /** * Whether this is a JSON Request * * @var bool */ -define('JSON_REQUEST', true); +define( 'JSON_REQUEST', true ); /** Include the bootstrap for setting up WordPress environment */ -include('./wp-load.php'); +include( './wp-load.php' ); -include_once(ABSPATH . 'wp-admin/includes/admin.php'); -include_once(ABSPATH . WPINC . '/class-wp-xmlrpc-server.php'); -include_once(ABSPATH . WPINC . '/class-wp-json-datetime.php'); -include_once(ABSPATH . WPINC . '/class-wp-json-server.php'); +include_once( ABSPATH . 'wp-admin/includes/admin.php' ); +include_once( ABSPATH . WPINC . '/class-wp-xmlrpc-server.php' ); +include_once( ABSPATH . WPINC . '/class-wp-json-datetime.php' ); +include_once( ABSPATH . WPINC . '/class-wp-json-server.php' ); // Allow for a plugin to insert a different class to handle requests. -$wp_json_server_class = apply_filters('wp_json_server_class', 'WP_JSON_Server'); +$wp_json_server_class = apply_filters( 'wp_json_server_class', 'WP_JSON_Server' ); + $wp_json_server = new $wp_json_server_class; // Fire off the request From a04df28ff5e3f6c3abf4a2342e684301d1048840 Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Tue, 27 May 2014 17:12:17 -0400 Subject: [PATCH 237/515] Remove argument from taxonomy routes Per IRC conversation on 2014.05.26, the /taxonomies base route will take precedence over the per-type endpoints. Removing the parameter will permit the existing routes to function, but moves towards having the /taxonomies base routes become the canonical ones. --- lib/class-wp-json-taxonomies.php | 46 +++++++++++--------------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 530d00df9b..4ca476754c 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -40,19 +40,14 @@ public function register_routes( $routes ) { /** * Get taxonomies * - * @param string $type Post type to get taxonomies for (optional) * @return array Taxonomy data */ - public function get_taxonomies( $type = '' ) { - if ( $type !== '' ) { - $taxonomies = get_object_taxonomies( $type, 'objects' ); - } else { - $taxonomies = get_taxonomies( '', 'objects' ); - } + public function get_taxonomies() { + $taxonomies = get_taxonomies( '', 'objects' ); $data = array(); foreach ($taxonomies as $tax_type => $value) { - $tax = $this->prepare_taxonomy( $value, $type, true ); + $tax = $this->prepare_taxonomy( $value, true ); if ( is_wp_error( $tax ) ) continue; @@ -66,34 +61,28 @@ public function get_taxonomies( $type = '' ) { * Get taxonomies * * @param string $taxonomy Taxonomy slug - * @param string $type Post type to get taxonomies for (optional) * @return array Taxonomy data */ - public function get_taxonomy( $taxonomy, $type = '' ) { + public function get_taxonomy( $taxonomy ) { $tax = get_taxonomy( $taxonomy ); if ( empty( $tax ) ) return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); - return $this->prepare_taxonomy( $tax, $type ); + return $this->prepare_taxonomy( $tax ); } /** * Prepare a taxonomy for serialization * * @param stdClass $taxonomy Taxonomy data - * @param string $type Post type to get taxonomies for * @param boolean $_in_collection Are we in a collection? * @return array Taxonomy data */ - protected function prepare_taxonomy( $taxonomy, $type, $_in_collection = false ) { + protected function prepare_taxonomy( $taxonomy, $_in_collection = false ) { if ( $taxonomy->public === false ) return new WP_Error( 'json_cannot_read_taxonomy', __( 'Cannot view taxonomy' ), array( 'status' => 403 ) ); - if ( $type !== '' ) { - $base_url = '/posts/types/' . $type . '/taxonomies/' . $taxonomy->name; - } else { - $base_url = '/taxonomies/' . $taxonomy->name; - } + $base_url = '/taxonomies/' . $taxonomy->name; $data = array( 'name' => $taxonomy->label, @@ -136,10 +125,9 @@ public function add_taxonomy_data( $data, $type ) { * Get terms for a post type * * @param string $taxonomy Taxonomy slug - * @param string $type Post type (optional) * @return array Term collection */ - public function get_terms( $taxonomy, $type = '' ) { + public function get_terms( $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); @@ -152,7 +140,7 @@ public function get_terms( $taxonomy, $type = '' ) { $data = array(); foreach ($terms as $term) { - $data[] = $this->prepare_term( $term, $type ); + $data[] = $this->prepare_term( $term ); } return $data; } @@ -162,11 +150,10 @@ public function get_terms( $taxonomy, $type = '' ) { * * @param string $taxonomy Taxonomy slug * @param string $term Term slug - * @param string $type Post type (optional) * @param string $context Context (view/view-parent) * @return array Term entity */ - public function get_term( $taxonomy, $term, $type = '', $context = 'view' ) { + public function get_term( $taxonomy, $term, $context = 'view' ) { if ( ! taxonomy_exists( $taxonomy ) ) return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); @@ -174,7 +161,7 @@ public function get_term( $taxonomy, $term, $type = '', $context = 'view' ) { if ( empty( $data ) ) return new WP_Error( 'json_taxonomy_invalid_term', __( 'Invalid term ID.' ), array( 'status' => 404 ) ); - return $this->prepare_term( $data, $type, $context ); + return $this->prepare_term( $data, $context ); } /** @@ -202,12 +189,9 @@ public function add_term_data( $data, $post, $context ) { * @param array|object $term The unprepared term data * @return array The prepared term data */ - protected function prepare_term( $term, $type, $context = 'view' ) { - if ( $type !== '' ) { - $base_url = '/posts/types/' . $type . '/taxonomies/' . $term->taxonomy . '/terms'; - } else { - $base_url = '/taxonomies/' . $term->taxonomy . '/terms'; - } + protected function prepare_term( $term, $context = 'view' ) { + $base_url = '/taxonomies/' . $term->taxonomy . '/terms'; + $data = array( 'ID' => (int) $term->term_taxonomy_id, 'name' => $term->name, @@ -225,7 +209,7 @@ protected function prepare_term( $term, $type, $context = 'view' ) { ); if ( ! empty( $data['parent'] ) && $context === 'view' ) { - $data['parent'] = $this->get_term( $type, $term->taxonomy, $data['parent'], 'view-parent' ); + $data['parent'] = $this->get_term( $term, $term->taxonomy, $data['parent'], 'view-parent' ); } elseif ( empty( $data['parent'] ) ) { $data['parent'] = null; From b48eae9a29123badb1973506620581e1c7e09a32 Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Tue, 27 May 2014 17:50:20 -0400 Subject: [PATCH 238/515] Update documentation to reflect /taxonomies routes - Added routes/taxonomies.md defining existing taxonomy routes - Updated guides/getting-started.md to clarify available methods (GET) - Updated the schema.md file to reflect the base /taxonomy endpoints --- docs/guides/getting-started.md | 24 +++++-------------- docs/routes/taxonomies.md | 42 ++++++++++++++++++++++++++++++++++ docs/schema.md | 4 ++-- 3 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 docs/routes/taxonomies.md diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index b5d4050330..9916ae20d2 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -176,32 +176,20 @@ And here's what we get back: "\/taxonomies\/": { "supports": [ "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "accepts_json": true + "GET" + ] }, "\/taxonomies\/\/terms": { "supports": [ "HEAD", - "GET", - "POST" - ], - "accepts_json": true + "GET" + ] }, "\/taxonomies\/\/terms\/": { "supports": [ "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "accepts_json": true + "GET" + ] }, "\/users": { "supports": [ diff --git a/docs/routes/taxonomies.md b/docs/routes/taxonomies.md new file mode 100644 index 0000000000..522e83caa9 --- /dev/null +++ b/docs/routes/taxonomies.md @@ -0,0 +1,42 @@ +Taxonomies +========== + + +Retrieve All Taxonomies +----------------------- +The Taxonomies endpoint returns a collection containing objects for each of the +site's registered taxonomies. + + GET /taxonomies + + +### Response +The response is a collection document containing all registered taxonomies. + + +Retrieve a Taxonomy +------------------- + + GET /taxonomy/ + +### Response +The response is a Taxonomy entity containing the requested Taxonomy, if available. + + +Retrieve Terms for a Taxonomy +----------------------------- + + GET /taxonomy//terms + +### Response +The response is a collection of taxonomy terms for the specified Taxonomy, if +available. + +Retrieve a Taxonomy Term +------------------------ + + GET /taxonomy//terms/ + +### Response +The response is a Taxonomy entity object containing the Taxonomy with the +requested ID, if available. diff --git a/docs/schema.md b/docs/schema.md index 51e895deb5..268dcb1687 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -411,8 +411,8 @@ representation. "count": 7, "meta": { "links": { - "collection": "http:\/\/example.com\/wp-json\/posts\/types\/post\/taxonomies\/category\/terms", - "self": "http:\/\/example.com\/wp-json\/posts\/types\/post\/taxonomies\/category\/terms\/1" + "collection": "http:\/\/example.com\/wp-json\/taxonomies\/category\/terms", + "self": "http:\/\/example.com\/wp-json\/taxonomies\/category\/terms\/1" } } } From ed5ad2a84fa57c669fe9b0472ab27bd5cae1a281 Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Wed, 28 May 2014 00:02:27 -0400 Subject: [PATCH 239/515] Include new client JS from github.io - see #179 --- plugin.php | 2 +- wp-api.js | 265 ----------------------------------------------------- 2 files changed, 1 insertion(+), 266 deletions(-) delete mode 100644 wp-api.js diff --git a/plugin.php b/plugin.php index 3571b043fc..1732d3716e 100644 --- a/plugin.php +++ b/plugin.php @@ -206,7 +206,7 @@ function json_api_deactivation( $network_wide ) { * Register our API Javascript helpers */ function json_register_scripts() { - wp_register_script( 'wp-api', plugins_url( '/wp-api.js', __FILE__ ), array( 'jquery', 'backbone', 'underscore' ), '0.6', true ); + wp_register_script( 'wp-api', 'http://wp-api.github.io/client-js/build/js/wp-api.js', array( 'jquery', 'backbone', 'underscore' ), '1.1', true ); wp_localize_script( 'wp-api', 'wpApiOptions', array( 'base' => json_url(), 'nonce' => wp_create_nonce( 'wp_json' ) ) ); } add_action( 'wp_enqueue_scripts', 'json_register_scripts', -100 ); diff --git a/wp-api.js b/wp-api.js deleted file mode 100644 index 41fbdefdad..0000000000 --- a/wp-api.js +++ /dev/null @@ -1,265 +0,0 @@ -(function ($) { - window.wp = window.wp || {}; - var api = wp.api = wp.api || {}; - - // Date parsing code - // From: https://github.com/csnover/js-iso8601 - // Copyright 2011 Colin Snover, MIT Licensed - var origParse = Date.parse, numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; - api.parseISO8601 = function (date) { - var timestamp, struct, minutesOffset = 0; - - // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string - // before falling back to any implementation-specific date parsing, so that’s what we do, even if native - // implementations could be faster - // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm - 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))) { - // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC - for (var i = 0, k; (k = numericKeys[i]); ++i) { - struct[k] = +struct[k] || 0; - } - - // allow undefined days and months - struct[2] = (+struct[2] || 1) - 1; - struct[3] = +struct[3] || 1; - - if (struct[8] !== 'Z' && struct[9] !== undefined) { - minutesOffset = struct[10] * 60 + struct[11]; - - if (struct[9] === '+') { - minutesOffset = 0 - minutesOffset; - } - } - - timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]); - } - else { - timestamp = origParse ? origParse(date) : NaN; - } - - return timestamp; - }; - - // ECMAScript 5 shim, from MDN - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString - if ( !Date.prototype.toISOString ) { - var pad = function (number) { - var r = String(number); - if ( r.length === 1 ) { - r = '0' + r; - } - return r; - }; - - Date.prototype.toISOString = function() { - return this.getUTCFullYear() - + '-' + pad( this.getUTCMonth() + 1 ) - + '-' + pad( this.getUTCDate() ) - + 'T' + pad( this.getUTCHours() ) - + ':' + pad( this.getUTCMinutes() ) - + ':' + pad( this.getUTCSeconds() ) - + '.' + String( (this.getUTCMilliseconds()/1000).toFixed(3) ).slice( 2, 5 ) - + 'Z'; - }; - } - - // Set the root URL from the global variable, if available - api.root = ""; - if (wpApiOptions) { - api.root = wpApiOptions.base; - api.nonce = wpApiOptions.nonce; - } - - /** - * wp.api.models - * - * Model container - */ - api.models = {}; - - /** - * wp.api.models.Base - * - * Base model for API objects - */ - api.models.Base = Backbone.Model.extend({ - sync: function (method, model, options) { - var options = options || {}; - - var beforeSend = options.beforeSend; - options.beforeSend = function(xhr) { - xhr.setRequestHeader('X-WP-Nonce', api.nonce); - - if (beforeSend) { - return beforeSend.apply(this, arguments); - } - }; - - return Backbone.sync(method, model, options); - } - }); - - /** - * wp.api.models.User - * - * User Entity model - */ - api.models.User = api.models.Base.extend({ - urlRoot: api.root + "/users", - - defaults: { - ID: 0, - name: "", - slug: "", - URL: "", - avatar: "", - meta: { - links: {} - } - }, - - avatar: function (size) { - return this.get('avatar') + '&s=' + size; - } - }); - - var parseable_dates = [ 'date', 'modified' ]; - - /** - * wp.api.models.Post - * - * Post Entity model - */ - api.models.Post = api.models.Base.extend({ - // Technically, these are probably not needed - defaults: function () { - return { - ID: 0, - title: "", - status: "draft", - type: "post", - author: new api.models.User(), - content: "", - parent: 0, - link: "", - date: new Date(), - // date_gmt: new Date(), - modified: new Date(), - // modified_gmt: new Date(), - format: "standard", - slug: "", - guid: "", - excerpt: "", - menu_order: 0, - comment_status: "open", - ping_status: "open", - sticky: false, - date_tz: "Etc/UTC", - modified_tz: "Etc/UTC", - terms: {}, - post_meta: {}, - meta: { - links: {} - } - }; - }, - - idAttribute: "ID", - - urlRoot: api.root + "/posts", - - /** - * Serialize the entity - * - * Overriden for correct date handling - * @return {!Object} Serializable attributes - */ - toJSON: function () { - var attributes = _.clone( this.attributes ); - - // Remove GMT dates in favour of our native Date objects - // The API only requires one of `date` and `date_gmt`, so this is - // safe for use. - delete attributes.date_gmt; - delete attributes.modified_gmt; - - // Serialize Date objects back into 8601 strings - _.each( parseable_dates, function ( key ) { - attributes[ key ] = attributes[ key ].toISOString(); - }); - - return attributes; - }, - - /** - * Unserialize the entity - * - * Overriden for correct date handling - * @param {!Object} response Attributes parsed from JSON - * @param {!Object} options Request options - * @return {!Object} Fully parsed attributes - */ - parse: function ( response, options ) { - // Parse dates into native Date objects - _.each( parseable_dates, function ( key ) { - if ( ! ( key in response ) ) - return; - - var timestamp = api.parseISO8601( response[ key ] ); - response[ key ] = new Date( timestamp ); - }); - - // Remove GMT dates in favour of our native Date objects - delete response.date_gmt; - delete response.modified_gmt; - - // Parse the author into a User object - response.author = new api.models.User(response.author); - - return response; - }, - - /** - * Get parent post - * - * @return {wp.api.models.Post} Parent post, null if not found - */ - parent: function () { - var parent = this.get('parent'); - - // Return null if we don't have a parent - if (parent === 0) { - return null; - } - - // Can we get this from its collection? - if (this.collection) { - return this.collection.get(parent); - } - else { - // Otherwise, get the post directly - var post = new api.models.Post({ - id: parent - }); - - // Note that this acts asynchronously - api.models.post.fetch(); - return post; - } - } - }); - - /** - * wp.api.collections - */ - api.collections = {}; - - /** - * wp.api.collections.Posts - */ - api.collections.Posts = Backbone.Collection.extend({ - url: api.root + "/posts", - - model: api.models.Post - }); -})(jQuery); \ No newline at end of file From dfbd822def1c6ce52ee83dd7b77da3256fd24bfb Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Wed, 28 May 2014 00:10:23 -0400 Subject: [PATCH 240/515] Properly setup client JS settings --- plugin.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin.php b/plugin.php index 1732d3716e..dd86f7a329 100644 --- a/plugin.php +++ b/plugin.php @@ -207,7 +207,9 @@ function json_api_deactivation( $network_wide ) { */ function json_register_scripts() { wp_register_script( 'wp-api', 'http://wp-api.github.io/client-js/build/js/wp-api.js', array( 'jquery', 'backbone', 'underscore' ), '1.1', true ); - wp_localize_script( 'wp-api', 'wpApiOptions', array( 'base' => json_url(), 'nonce' => wp_create_nonce( 'wp_json' ) ) ); + + $settings = array( 'root' => esc_url_raw( home_url( 'wp-json' ) ) ); + wp_localize_script( 'wp-api', 'WP_API_Settings', $settings ); } add_action( 'wp_enqueue_scripts', 'json_register_scripts', -100 ); From 28615098bb6dfd780afa27ae124fc8292cb6bf33 Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Wed, 28 May 2014 00:14:01 -0400 Subject: [PATCH 241/515] Re-localize nonce for JS --- plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.php b/plugin.php index dd86f7a329..ace968bfda 100644 --- a/plugin.php +++ b/plugin.php @@ -208,7 +208,7 @@ function json_api_deactivation( $network_wide ) { function json_register_scripts() { wp_register_script( 'wp-api', 'http://wp-api.github.io/client-js/build/js/wp-api.js', array( 'jquery', 'backbone', 'underscore' ), '1.1', true ); - $settings = array( 'root' => esc_url_raw( home_url( 'wp-json' ) ) ); + $settings = array( 'root' => esc_url_raw( home_url( 'wp-json' ) ), 'nonce' => wp_create_nonce( 'wp_json' ) ); wp_localize_script( 'wp-api', 'WP_API_Settings', $settings ); } add_action( 'wp_enqueue_scripts', 'json_register_scripts', -100 ); From 8aede859a6461fdd45070933b06aac95c2ff1f33 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 28 May 2014 21:21:08 +1000 Subject: [PATCH 242/515] Add format comparison --- docs/comparison.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/comparison.md b/docs/comparison.md index de5ce49f5b..fd8cd444ed 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -131,3 +131,55 @@ taxonomies. | Retrieve a page by slug | X | X | | | Edit a page | X | X | X | | Delete a page | X | X | X | + +## Formats + +### Posts + +See [read format](http://wp-api.github.io/comparison/post.html) vs +[edit format](http://wp-api.github.io/comparison/post-edit.html). The edit +format is triggered in both by appending `?context=edit` to the resource. + +**Key differences:** + +* WP.com API changes the format of the `content`/`excerpt` fields if in edit + mode. WP API separates the fields into `content`/`content_raw` and + `excerpt`/`excerpt_raw` respectively. + + With combined fields, clients cannot display the existing content and also + allow editing without sending multiple requests. + +* WP API links to all resources related to the current one, including the + author endpoint and the related collection (e.g. `/posts` or `/media`). WP.com + API contains a limited subset of links, and does not include author or + collection links. + +* WP.com API embeds all attachments in the post. WP API does not currently do + this. Attachments belonging to the post can still be queried via + `/media?filter[post_parent]=`. + +## Media + +See [read format](http://wp-api.github.io/comparison/media.html) vs +[edit format](http://wp-api.github.io/comparison/media-edit.html). The edit +format is triggered in both by appending `?context=edit` to the resource. + +**Key differences:** + +* WP API does not expose the caption. This is a bug, and will be fixed in 1.1. + +* WP API treats media as a full custom post type, and includes all post-related + data. WP.com API treats media as a different type, and only gives a limited + subset of all data back. + +* WP.com API does not expose intermediate size URLs, as Photon is used for + WP.com and Jetpack image resizing. WP API exposes all intermediate size URLs + where possible. + +* WP.com API does not link to any related resources. WP API links to all related + resources, such as the parent, the collection and the author. + +* WP.com API does not expose any authorship data, instead relying on clients to + query for the post parent, and work out the authorship of the parent post. + WP API treats this as normal post data, with its own author, and embeds the + entire user object for the view context (as with normal posts). From cd1a6854dc9c00f8bef311ec2411dd6502bdf3f8 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 28 May 2014 21:22:45 +1000 Subject: [PATCH 243/515] Separate functionality tables into separate section --- docs/comparison.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/comparison.md b/docs/comparison.md index fd8cd444ed..f246659474 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -5,12 +5,14 @@ API) to the other WordPress JSON API projects. Included in this comparison are the [WordPress.com JSON REST API](http://developer.wordpress.com/docs/api/), and the built-in XML-RPC API. -## Key +## Functionality + +### Key * **M**: Multisite only * **P**: Available via (official) plugin * **~**: Partially available -## Authentication +### Authentication | Feature | WP API | WP.com JSON API | XML-RPC API | |:-------------------------|:-------|:----------------|:-----------:| @@ -24,7 +26,7 @@ Custom authentication can be used with WP API via the `determine_current_user` filter. With the XML-RPC API, only one custom handler can be added at a time via subclassing. -## Site +### Site | Feature | WP API | WP.com JSON API | XML-RPC API | |:-------------------------|:-------|:----------------|:-----------:| @@ -37,7 +39,7 @@ subclassing. The XML-RPC API only exposes a limited subset of the available options in WordPress. -## Users +### Users | Feature | WP API | WP.com JSON API | XML-RPC API | |:-------------------------|:-------|:----------------|:-----------:| @@ -48,7 +50,7 @@ WordPress. | Edit a user | X | | X | | Delete a user | X | | | -## Posts +### Posts | Feature | WP API | WP.com JSON API | XML-RPC API | |:-------------------------|:-------|:----------------|:-----------:| @@ -68,7 +70,7 @@ accessing via WP Query: GET /wp-json/posts?filter[name]=my-post-slug -## Comments +### Comments | Feature | WP API | WP.com JSON API | XML-RPC API | |:-------------------------|:-------|:----------------|:-----------:| @@ -80,7 +82,7 @@ accessing via WP Query: | Edit a comment | | X | X | | Delete a comment | X | X | X | -# Post-Related Data +### Post-Related Data | Feature | WP API | WP.com JSON API | XML-RPC API | |:-------------------------|:-------|:----------------|:-----------:| @@ -89,7 +91,7 @@ accessing via WP Query: | List all post statuses | X | | X | | Retrieve post status | | | | -## Taxonomies +### Taxonomies | Feature | WP API | WP.com JSON API | XML-RPC API | |:-------------------------|:-------|:---------------:|:-----------:| @@ -104,14 +106,14 @@ accessing via WP Query: Taxonomies are defined in code, so no APIs can create, modify, or delete taxonomies. -## Revisions +### Revisions | Feature | WP API | WP.com JSON API | XML-RPC API | |:-------------------------|:-------|:---------------:|:-----------:| | Get revisions for a post | X | | X | | Restore revision | | | X | -## Media +### Media | Feature | WP API | WP.com JSON API | XML-RPC API | |:-------------------------|:-------|:---------------:|:-----------:| @@ -121,7 +123,7 @@ taxonomies. | Edit an attachment | X | X | X | | Delete an attachment | X | X | X | -## Pages +### Pages | Feature | WP API | WP.com JSON API | XML-RPC API | |:-------------------------|:-------|:---------------:|:-----------:| From ee30b978c9775f5beabff3819390a39d865d0eeb Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 28 May 2014 23:51:33 +1000 Subject: [PATCH 244/515] Add general comparison section --- docs/comparison.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/comparison.md b/docs/comparison.md index f246659474..5a8e103215 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -26,6 +26,26 @@ Custom authentication can be used with WP API via the `determine_current_user` filter. With the XML-RPC API, only one custom handler can be added at a time via subclassing. +### General + +| Feature | WP API | WP.com JSON API | XML-RPC API | +|:-------------------------|:-------|:----------------|:-----------:| +| Add custom routes | X | | X | +| Modify responses | X | | X | +| Support for other formats| X | | | +| Internally usable | X | | | +| Plugin/theme support | X | ~ | | +| Usable while offline | X | | X | + +WP API supports fully replacing JSON responses with an alternative format, such +as MsgPack, as well as replacing the HTTP layers with others. The API can also +be called internally as normal functions, without causing side effects (HTTP +headers, premature exit, etc). + +WP API supports use in the browser by themes/plugins simply by enqueuing the +JS client. WP.com API requires writing custom code to communicate with the API. + + ### Site | Feature | WP API | WP.com JSON API | XML-RPC API | From 633589c7455d7ba073282ace3a781c1afd54e7c5 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 28 May 2014 23:52:47 +1000 Subject: [PATCH 245/515] Add documentation on authentication methods --- docs/authentication.md | 109 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 docs/authentication.md diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000000..c0051d524a --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,109 @@ +Authentication +============== + +There are two options for authenticating with the API. The basic choice boils +down to: + +* Are you a plugin/theme running on the site? Use **cookie authentication** +* Are you a desktop/web/mobile client accessing the site externally? Use + **OAuth authentication** + + +Cookie Authentication +--------------------- +Cookie authentication is the basic authentication method included with +WordPress. When you log in to your dashboard, this sets up the cookies correctly +for you, so plugin and theme developers need only to have a logged-in user. + +However, WP API includes a technique called [nonces][] to avoid [CSRF][] issues. +This prevents other sites from forcing you to perform actions without explicitly +intending to do so. This requires slightly special handling for the API. + +For developers using the built-in Javascript API, this is handled automatically +for you. This is the recommended way to use the API for plugins and themes. +Custom data models can extend `wp.api.models.Base` to ensure this is sent +correctly for any custom requests. + +For developers making manual Ajax requests, the nonce will need to be passed +with each request. The API uses nonces with the action set to `wp_api`. These +can then be passed to the API via the `_wp_json_nonce` data parameter (either +POST data or in the query for GET requests), or via the `X-WP-Nonce` header. + +As an example, this is how the built-in Javascript client creates the nonce: + +```php +wp_localize_script( 'wp-api', 'wpApiOptions', array( 'base' => json_url(), 'nonce' => wp_create_nonce( 'wp_json' ) ) ); +``` + +This is then used in the base model: + +```javascript +options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-WP-Nonce', wpApiOptions.nonce); + + if (beforeSend) { + return beforeSend.apply(this, arguments); + } +}; +``` + +[nonces]: http://codex.wordpress.org/WordPress_Nonces +[CSRF]: http://en.wikipedia.org/wiki/Cross-site_request_forgery + + +OAuth Authentication +-------------------- +OAuth authentication is the main authentication handler used for external +clients. With OAuth authentication, users still only ever log in via the normal +WP login form, and then authorize clients to act on their behalf. Clients are +then issued with OAuth tokens that enable them to access the API. This access +can be revoked by users at any point. + +OAuth authentication uses the [OAuth 1.0a specification][oauth] (published as +RFC5849) and requires installing the [OAuth plugin][oauth-plugin] on the site. +(This plugin will be included with the API when merged into core.) + +Once you have WP API and the OAuth server plugins activated on your server, +you'll need to create a "consumer". This is an identifier for the application, +and includes a "key" and "secret", both needed to link to your site. + +To create the consumer, run the following on your server: +```bash +$ wp oauth1 add + +ID: 4 +Key: sDc51JgH2mFu +Secret: LnUdIsyhPFnURkatekRIAUfYV7nmP4iF3AVxkS5PRHPXxgOW +``` + +This key and secret is your consumer key and secret, and needs to be used +throughout the authorization process. Currently no UI exists to manage this, +however this is planned for a future release. + +For examples on how to use this, both the [CLI client][client-cli] and the +[API console][api-console] make use of the OAuth functionality, and are a great +starting point. + +[oauth]: http://tools.ietf.org/html/rfc5849 +[oauth-plugin]: https://github.com/WP-API/OAuth1 +[client-cli]: https://github.com/WP-API/client-cli +[api-console]: https://github.com/WP-API/api-console + + +Basic Authentication +-------------------- +Basic Authentication is an optional authentication handler for external clients. +Due to the complexity of OAuth authentication, Basic authentication can be +useful during development. However, Basic authentication requires passing your +username and password on every request, as well as giving your credentials to +clients, so it is heavily discouraged for production use. + +Basic authentication uses [HTTP Basic Authentication][http-basic] (published as +RFC2617) and requires installing the [Basic Auth plugin][basic-auth-plugin]. + +To use Basic authentication, simply pass the username and password with each +request through the `Authorization` header. This value should be encoded as per +the HTTP Basic specification. + +[http-basic]: https://tools.ietf.org/html/rfc2617 +[basic-auth-plugin]: https://github.com/WP-API/Basic-Auth From 1559ee569c48171e6dc219c9e64b1848dd849934 Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Wed, 28 May 2014 11:34:43 -0400 Subject: [PATCH 246/515] `json_url` filter should be passed $scheme --- plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.php b/plugin.php index 3571b043fc..e6ba5708ba 100644 --- a/plugin.php +++ b/plugin.php @@ -385,7 +385,7 @@ function get_json_url( $blog_id = null, $path = '', $scheme = 'json' ) { $url = add_query_arg( 'json_route', $path, $url ); } - return apply_filters( 'json_url', $url, $path, $blog_id ); + return apply_filters( 'json_url', $url, $path, $blog_id, $scheme ); } /** From 5f41e253d7947c877bc81dab3198cfbc3620179a Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Wed, 28 May 2014 12:47:09 -0400 Subject: [PATCH 247/515] Add unit tests for /taxonomies routes --- tests/test_json_taxonomies.php | 105 +++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/test_json_taxonomies.php diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php new file mode 100644 index 0000000000..16ada5e36b --- /dev/null +++ b/tests/test_json_taxonomies.php @@ -0,0 +1,105 @@ +user = $this->factory->user->create(); + wp_set_current_user( $this->user ); + + $this->fake_server = $this->getMock('WP_JSON_Server'); + $this->endpoint = new WP_JSON_Taxonomies( $this->fake_server ); + } + + public function test_get_taxonomies() { + $response = $this->endpoint->get_taxonomies(); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $taxonomies = get_taxonomies( '', 'objects' ); + + + // All public taxonomies should be represented + function is_public( $taxonomy ) { + return $taxonomy->public !== false; + } + $public_taxonomies = array_values( array_filter( $taxonomies, 'is_public' ) ); + $this->assertEquals( count( $public_taxonomies ), count( $data ) ); + + // Check each key in $data against those in $public_taxonomies + foreach ( array_keys( $data ) as $key ) { + $this->assertEquals( $public_taxonomies[$key]->label, $data[$key]['name'] ); + $this->assertEquals( $public_taxonomies[$key]->name, $data[$key]['slug'] ); + $this->assertEquals( $public_taxonomies[$key]->hierarchical, $data[$key]['hierarchical'] ); + $this->assertEquals( $public_taxonomies[$key]->show_tagcloud, $data[$key]['show_cloud'] ); + } + } + + public function test_get_taxonomy() { + $response = $this->endpoint->get_taxonomy( 'category' ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $category = get_taxonomy( 'category' ); + + $this->assertEquals( $category->label, $data['name'] ); + $this->assertEquals( $category->name, $data['slug'] ); + $this->assertEquals( $category->show_tagcloud, $data['show_cloud'] ); + $this->assertEquals( $category->hierarchical, $data['hierarchical'] ); + } + + public function test_get_terms() { + $response = $this->endpoint->get_terms( 'category' ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + + $args = array( + 'hide_empty' => false, + ); + $categories = get_terms( 'category', $args ); + + $this->assertEquals( count( $categories ), count( $data ) ); + + $this->assertEquals( $categories[0]->term_id, $data[0]['ID'] ); + $this->assertEquals( $categories[0]->name, $data[0]['name'] ); + $this->assertEquals( $categories[0]->slug, $data[0]['slug']); + $this->assertEquals( $categories[0]->description, $data[0]['description']); + $this->assertEquals( $categories[0]->count, $data[0]['count']); + } + + public function test_get_term() { + $response = $this->endpoint->get_term( 'category', 1 ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $category = get_term( 1, 'category' ); + + $this->assertEquals( $category->term_id, $data['ID'] ); + $this->assertEquals( $category->name, $data['name'] ); + $this->assertEquals( $category->slug, $data['slug'] ); + $this->assertEquals( $category->description, $data['description'] ); + $this->assertEquals( $category->count, $data['count'] ); + } +} From f47115272fce5a5a1529339105b3ba0e0526593d Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Wed, 28 May 2014 14:27:49 -0400 Subject: [PATCH 248/515] Fix regression causing all taxonomies to be returned for all types While the taxonomies routes are now type-independent, the post type routes (e.g. /posts/types/post) return embedded taxonomy objects for each taxonomy associated with that type. Removing the "types" argument from the get_taxonomies method caused a regression where ALL taxonomies would be returned for any post type endpoint. --- lib/class-wp-json-taxonomies.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 4ca476754c..328e5d58bf 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -40,10 +40,15 @@ public function register_routes( $routes ) { /** * Get taxonomies * + * @param string $type A specific type for which to retrieve taxonomies * @return array Taxonomy data */ - public function get_taxonomies() { - $taxonomies = get_taxonomies( '', 'objects' ); + public function get_taxonomies( $type = '' ) { + if ( '' === $type ) { + $taxonomies = get_taxonomies( '', 'objects' ); + } else { + $taxonomies = get_object_taxonomies( $type, 'objects' ); + } $data = array(); foreach ($taxonomies as $tax_type => $value) { From 920a97d67ddbf71451287f26efa2689c830d2027 Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Wed, 28 May 2014 14:52:50 -0400 Subject: [PATCH 249/515] Add test for add_taxonomy_data regression This adds a test to verify that get_taxonomies only returns taxonomies for the provided post type if a type is passed into the function. This ensures that the taxonomies embedded into post type endpoint responses will be accurate. It also DRYs up the test code a bit to avoid repeating the methods for filtering a taxonomy collection down to only "public" members. --- tests/test_json_taxonomies.php | 50 +++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 16ada5e36b..0c1cddf810 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -20,6 +20,21 @@ public function setUp() { $this->endpoint = new WP_JSON_Taxonomies( $this->fake_server ); } + /** + * Utility function for use in get_public_taxonomies + */ + private function is_public( $taxonomy ) { + return $taxonomy->public !== false; + } + + /** + * Utility function to filter down to only public taxonomies + */ + private function get_public_taxonomies( $taxonomies ) { + // Pass through array_values to re-index after filtering + return array_values( array_filter( $taxonomies, array( $this, 'is_public' ) ) ); + } + public function test_get_taxonomies() { $response = $this->endpoint->get_taxonomies(); $this->assertNotInstanceOf( 'WP_Error', $response ); @@ -28,22 +43,31 @@ public function test_get_taxonomies() { $this->assertEquals( 200, $response->get_status() ); $data = $response->get_data(); - $taxonomies = get_taxonomies( '', 'objects' ); + $taxonomies = $this->get_public_taxonomies( get_taxonomies( '', 'objects' ) ); + $this->assertEquals( count( $taxonomies ), count( $data ) ); - - // All public taxonomies should be represented - function is_public( $taxonomy ) { - return $taxonomy->public !== false; + // Check each key in $data against those in $taxonomies + foreach ( array_keys( $data ) as $key ) { + $this->assertEquals( $taxonomies[$key]->label, $data[$key]['name'] ); + $this->assertEquals( $taxonomies[$key]->name, $data[$key]['slug'] ); + $this->assertEquals( $taxonomies[$key]->hierarchical, $data[$key]['hierarchical'] ); + $this->assertEquals( $taxonomies[$key]->show_tagcloud, $data[$key]['show_cloud'] ); } - $public_taxonomies = array_values( array_filter( $taxonomies, 'is_public' ) ); - $this->assertEquals( count( $public_taxonomies ), count( $data ) ); + } - // Check each key in $data against those in $public_taxonomies - foreach ( array_keys( $data ) as $key ) { - $this->assertEquals( $public_taxonomies[$key]->label, $data[$key]['name'] ); - $this->assertEquals( $public_taxonomies[$key]->name, $data[$key]['slug'] ); - $this->assertEquals( $public_taxonomies[$key]->hierarchical, $data[$key]['hierarchical'] ); - $this->assertEquals( $public_taxonomies[$key]->show_tagcloud, $data[$key]['show_cloud'] ); + public function test_get_taxonomies_with_types() { + foreach ( get_post_types() as $type ) { + $response = $this->endpoint->get_taxonomies( $type ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + + $taxonomies = $this->get_public_taxonomies( get_object_taxonomies( $type, 'objects' ) ); + + $this->assertEquals( count( $taxonomies ), count( $data ) ); } } From 791113881659bd8f0167e4b8585cdeb1a2d8678f Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Thu, 29 May 2014 01:53:12 -0400 Subject: [PATCH 250/515] Reinstate $types parameter for back-compat with 1.0 $types is only necessary in `get_taxonomies` and `add_taxonomy_data`; however, we need to leave it in the argumentss list for ALL methods in order to avoid breaking any plugins built to interoperate with v1.0. get_taxonomy (singular), prepare_taxonomy, get_terms, get_term, and prepare_term should all have their $type parameter removed in 2.0. --- lib/class-wp-json-taxonomies.php | 39 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 328e5d58bf..5a0ebbc356 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -40,11 +40,11 @@ public function register_routes( $routes ) { /** * Get taxonomies * - * @param string $type A specific type for which to retrieve taxonomies + * @param string|null $type A specific post type for which to retrieve taxonomies (optional) * @return array Taxonomy data */ - public function get_taxonomies( $type = '' ) { - if ( '' === $type ) { + public function get_taxonomies( $type = null ) { + if ( null === $type ) { $taxonomies = get_taxonomies( '', 'objects' ); } else { $taxonomies = get_object_taxonomies( $type, 'objects' ); @@ -52,7 +52,7 @@ public function get_taxonomies( $type = '' ) { $data = array(); foreach ($taxonomies as $tax_type => $value) { - $tax = $this->prepare_taxonomy( $value, true ); + $tax = $this->prepare_taxonomy( $value, null, true ); if ( is_wp_error( $tax ) ) continue; @@ -65,10 +65,11 @@ public function get_taxonomies( $type = '' ) { /** * Get taxonomies * + * @param string $type Post type to get taxonomies for (deprecated) * @param string $taxonomy Taxonomy slug * @return array Taxonomy data */ - public function get_taxonomy( $taxonomy ) { + public function get_taxonomy( $type, $taxonomy ) { $tax = get_taxonomy( $taxonomy ); if ( empty( $tax ) ) return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); @@ -80,10 +81,11 @@ public function get_taxonomy( $taxonomy ) { * Prepare a taxonomy for serialization * * @param stdClass $taxonomy Taxonomy data + * @param string|null $type Post type to get taxonomies for (deprecated) * @param boolean $_in_collection Are we in a collection? * @return array Taxonomy data */ - protected function prepare_taxonomy( $taxonomy, $_in_collection = false ) { + protected function prepare_taxonomy( $taxonomy, $type = null, $_in_collection = false ) { if ( $taxonomy->public === false ) return new WP_Error( 'json_cannot_read_taxonomy', __( 'Cannot view taxonomy' ), array( 'status' => 403 ) ); @@ -98,18 +100,13 @@ protected function prepare_taxonomy( $taxonomy, $_in_collection = false ) { 'hierarchical' => $taxonomy->hierarchical, 'meta' => array( 'links' => array( - 'archives' => json_url( $base_url . '/terms' ) + 'archives' => json_url( $base_url . '/terms' ), + 'collection' => json_url( '/taxonomies' ), + 'self' => json_url( $base_url ) ) ), ); - if ( $_in_collection ) { - $data['meta']['links']['self'] = json_url( $base_url ); - } - else { - $data['meta']['links']['collection'] = json_url( $base_url ); - } - return apply_filters( 'json_prepare_taxonomy', $data ); } @@ -132,7 +129,7 @@ public function add_taxonomy_data( $data, $type ) { * @param string $taxonomy Taxonomy slug * @return array Term collection */ - public function get_terms( $taxonomy ) { + public function get_terms( $type, $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); @@ -153,12 +150,13 @@ public function get_terms( $taxonomy ) { /** * Get term for a post type * + * @param string $type Post type (deprecated) * @param string $taxonomy Taxonomy slug * @param string $term Term slug * @param string $context Context (view/view-parent) * @return array Term entity */ - public function get_term( $taxonomy, $term, $context = 'view' ) { + public function get_term( $type, $taxonomy, $term, $context = 'view' ) { if ( ! taxonomy_exists( $taxonomy ) ) return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); @@ -166,7 +164,7 @@ public function get_term( $taxonomy, $term, $context = 'view' ) { if ( empty( $data ) ) return new WP_Error( 'json_taxonomy_invalid_term', __( 'Invalid term ID.' ), array( 'status' => 404 ) ); - return $this->prepare_term( $data, $context ); + return $this->prepare_term( $data, null, $context ); } /** @@ -182,7 +180,7 @@ public function add_term_data( $data, $post, $context ) { $terms = wp_get_object_terms( $post['ID'], $post_type_taxonomies ); $data['terms'] = array(); foreach ( $terms as $term ) { - $data['terms'][ $term->taxonomy ][] = $this->prepare_term( $term, $post['post_type'] ); + $data['terms'][ $term->taxonomy ][] = $this->prepare_term( $term ); } return $data; @@ -192,9 +190,10 @@ public function add_term_data( $data, $post, $context ) { * Prepare term data for serialization * * @param array|object $term The unprepared term data + * @param string|null $type Post type to get taxonomies for (deprecated) * @return array The prepared term data */ - protected function prepare_term( $term, $context = 'view' ) { + protected function prepare_term( $term, $type = null, $context = 'view' ) { $base_url = '/taxonomies/' . $term->taxonomy . '/terms'; $data = array( @@ -214,7 +213,7 @@ protected function prepare_term( $term, $context = 'view' ) { ); if ( ! empty( $data['parent'] ) && $context === 'view' ) { - $data['parent'] = $this->get_term( $term, $term->taxonomy, $data['parent'], 'view-parent' ); + $data['parent'] = $this->get_term( null, $term->taxonomy, $data['parent'], 'view-parent' ); } elseif ( empty( $data['parent'] ) ) { $data['parent'] = null; From 66e1f98f56d08434199805f3b63ec518dfaabeca Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Thu, 29 May 2014 02:43:17 -0400 Subject: [PATCH 251/515] Add new methods for base /taxonomies routes & update tests Because the new /taxonomies route have no $type parameter, and $type cannot be removed without breaking back-compat, we need to add a new set of route methods for the new, "correct" /taxonomies routes. To minimize confusion, the old route method names were left as is, and new methods added for each method which had previously taken a $type. All the old methods are marked @deprecated and have a @see reference to their successors. New methods: - `get_taxonomy` now wraps the new `get_taxonomy_object` - `prepare_taxonomy` now wraps the new `prepare_taxonomy_object` - `get_terms` now wraps the new `get_taxonomy_terms` - `get_term` now wraps the new `get_taxonomy_terms` - `prepare_term` now wraps the new `prepare_taxonomy_term` The existing unit tests were changed to point to these new route methods. --- lib/class-wp-json-taxonomies.php | 92 ++++++++++++++++++++++++++++---- tests/test_json_taxonomies.php | 12 ++--- 2 files changed, 87 insertions(+), 17 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 5a0ebbc356..b05ca9e2e9 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -25,13 +25,13 @@ public function register_routes( $routes ) { array( array( $this, 'get_taxonomies' ), WP_JSON_Server::READABLE ), ), '/taxonomies/(?P\w+)' => array( - array( array( $this, 'get_taxonomy' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_taxonomy_object' ), WP_JSON_Server::READABLE ), ), '/taxonomies/(?P\w+)/terms' => array( - array( array( $this, 'get_terms' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_taxonomy_terms' ), WP_JSON_Server::READABLE ), ), '/taxonomies/(?P\w+)/terms/(?P\w+)' => array( - array( array( $this, 'get_term' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_taxonomy_term' ), WP_JSON_Server::READABLE ), ), ); return array_merge( $routes, $tax_routes ); @@ -52,7 +52,7 @@ public function get_taxonomies( $type = null ) { $data = array(); foreach ($taxonomies as $tax_type => $value) { - $tax = $this->prepare_taxonomy( $value, null, true ); + $tax = $this->prepare_taxonomy_object( $value, true ); if ( is_wp_error( $tax ) ) continue; @@ -63,29 +63,56 @@ public function get_taxonomies( $type = null ) { } /** - * Get taxonomies + * Get taxonomies (legacy route with support for passing $type) + * + * @deprecated + * @see get_taxonomy_object * * @param string $type Post type to get taxonomies for (deprecated) * @param string $taxonomy Taxonomy slug * @return array Taxonomy data */ public function get_taxonomy( $type, $taxonomy ) { + return $this->get_taxonomy_object( $taxonomy ); + } + + /** + * Get taxonomies + * + * @param string $taxonomy Taxonomy slug + * @return array Taxonomy data + */ + public function get_taxonomy_object( $taxonomy ) { $tax = get_taxonomy( $taxonomy ); if ( empty( $tax ) ) return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); - return $this->prepare_taxonomy( $tax ); + return $this->prepare_taxonomy_object( $tax ); } /** * Prepare a taxonomy for serialization * + * @deprecated + * @see prepare_taxonomy_object + * * @param stdClass $taxonomy Taxonomy data * @param string|null $type Post type to get taxonomies for (deprecated) * @param boolean $_in_collection Are we in a collection? * @return array Taxonomy data */ protected function prepare_taxonomy( $taxonomy, $type = null, $_in_collection = false ) { + return $this->prepare_taxonomy_object( $taxonomy, $_in_collection ); + } + + /** + * Prepare a taxonomy object for serialization + * + * @param stdClass $taxonomy Taxonomy data + * @param boolean $_in_collection Are we in a collection? + * @return array Taxonomy data + */ + protected function prepare_taxonomy_object( $taxonomy, $_in_collection = false ) { if ( $taxonomy->public === false ) return new WP_Error( 'json_cannot_read_taxonomy', __( 'Cannot view taxonomy' ), array( 'status' => 403 ) ); @@ -124,12 +151,26 @@ public function add_taxonomy_data( $data, $type ) { } /** - * Get terms for a post type + * Get terms for a post type (legacy route with support for passing $type) + * + * @deprecated + * @see get_taxonomy_terms * + * @param string $type Post type for which to fetch taxonomies (deprecated) * @param string $taxonomy Taxonomy slug * @return array Term collection */ public function get_terms( $type, $taxonomy ) { + return $this->get_taxonomy_terms( $taxonomy ); + } + + /** + * Get all terms for a post type + * + * @param string $taxonomy Taxonomy slug + * @return array Term collection + */ + public function get_taxonomy_terms( $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); @@ -142,7 +183,7 @@ public function get_terms( $type, $taxonomy ) { $data = array(); foreach ($terms as $term) { - $data[] = $this->prepare_term( $term ); + $data[] = $this->prepare_taxonomy_term( $term ); } return $data; } @@ -150,6 +191,9 @@ public function get_terms( $type, $taxonomy ) { /** * Get term for a post type * + * @deprecated + * @see get_taxonomy_term + * * @param string $type Post type (deprecated) * @param string $taxonomy Taxonomy slug * @param string $term Term slug @@ -157,6 +201,18 @@ public function get_terms( $type, $taxonomy ) { * @return array Term entity */ public function get_term( $type, $taxonomy, $term, $context = 'view' ) { + return $this->get_taxonomy_term( $taxonomy, $term, $context ); + } + + /** + * Get term for a post type + * + * @param string $taxonomy Taxonomy slug + * @param string $term Term slug + * @param string $context Context (view/view-parent) + * @return array Term entity + */ + public function get_taxonomy_term( $taxonomy, $term, $context = 'view' ) { if ( ! taxonomy_exists( $taxonomy ) ) return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); @@ -164,7 +220,7 @@ public function get_term( $type, $taxonomy, $term, $context = 'view' ) { if ( empty( $data ) ) return new WP_Error( 'json_taxonomy_invalid_term', __( 'Invalid term ID.' ), array( 'status' => 404 ) ); - return $this->prepare_term( $data, null, $context ); + return $this->prepare_taxonomy_term( $data, $context ); } /** @@ -180,7 +236,7 @@ public function add_term_data( $data, $post, $context ) { $terms = wp_get_object_terms( $post['ID'], $post_type_taxonomies ); $data['terms'] = array(); foreach ( $terms as $term ) { - $data['terms'][ $term->taxonomy ][] = $this->prepare_term( $term ); + $data['terms'][ $term->taxonomy ][] = $this->prepare_taxonomy_term( $term ); } return $data; @@ -189,11 +245,25 @@ public function add_term_data( $data, $post, $context ) { /** * Prepare term data for serialization * + * @deprecated + * @see prepare_taxonomy_term + * * @param array|object $term The unprepared term data * @param string|null $type Post type to get taxonomies for (deprecated) * @return array The prepared term data */ protected function prepare_term( $term, $type = null, $context = 'view' ) { + return $this->prepare_taxonomy_term( $term, $context ); + } + + + /** + * Prepare term data for serialization + * + * @param array|object $term The unprepared term data + * @return array The prepared term data + */ + protected function prepare_taxonomy_term( $term, $context = 'view' ) { $base_url = '/taxonomies/' . $term->taxonomy . '/terms'; $data = array( @@ -213,7 +283,7 @@ protected function prepare_term( $term, $type = null, $context = 'view' ) { ); if ( ! empty( $data['parent'] ) && $context === 'view' ) { - $data['parent'] = $this->get_term( null, $term->taxonomy, $data['parent'], 'view-parent' ); + $data['parent'] = $this->get_taxonomy_term( $term->taxonomy, $data['parent'], 'view-parent' ); } elseif ( empty( $data['parent'] ) ) { $data['parent'] = null; diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 0c1cddf810..4f8acd941f 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -71,8 +71,8 @@ public function test_get_taxonomies_with_types() { } } - public function test_get_taxonomy() { - $response = $this->endpoint->get_taxonomy( 'category' ); + public function test_get_taxonomy_object() { + $response = $this->endpoint->get_taxonomy_object( 'category' ); $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); @@ -87,8 +87,8 @@ public function test_get_taxonomy() { $this->assertEquals( $category->hierarchical, $data['hierarchical'] ); } - public function test_get_terms() { - $response = $this->endpoint->get_terms( 'category' ); + public function test_get_taxonomy_terms() { + $response = $this->endpoint->get_taxonomy_terms( 'category' ); $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); @@ -110,8 +110,8 @@ public function test_get_terms() { $this->assertEquals( $categories[0]->count, $data[0]['count']); } - public function test_get_term() { - $response = $this->endpoint->get_term( 'category', 1 ); + public function test_get_taxonomy_term() { + $response = $this->endpoint->get_taxonomy_term( 'category', 1 ); $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); From b3c628ce52c7d5b7d15978d351947e4333c99437 Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Thu, 29 May 2014 02:55:32 -0400 Subject: [PATCH 252/515] Embed associated post type records in Taxonomy objects This is the first pass at embedding a list of associated post types in taxonomy records, as described in #197. Types are NOT embedded if they would be added to a taxonomy record that is itself embedded. @rmccue has proposed changing how the $_in_taxonomy behavior works to use a "$context" variable, as in other methods; this commit should be considered a work-in-progress until he has signed off on it. --- lib/class-wp-json-posts.php | 23 ++++++++++++++++++++--- lib/class-wp-json-taxonomies.php | 9 ++++++--- plugin.php | 3 ++- tests/test_json_taxonomies.php | 14 ++++++++++++++ 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 941c79693e..a06ed0f297 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -519,9 +519,10 @@ public function get_post_types() { * * @param string|object $type Type name, or type object (internal use) * @param boolean $_in_collection Is this in a collection? (internal use) + * @param boolean $_in_taxonomy Is this request being added to a taxonomy record? (internal use) * @return array Post type data */ - public function get_post_type( $type, $_in_collection = false ) { + public function get_post_type( $type, $_in_collection = false, $_in_taxonomy = false ) { if ( ! is_object( $type ) ) $type = get_post_type_object($type); @@ -542,7 +543,7 @@ public function get_post_type( $type, $_in_collection = false ) { ), ); - if ( $_in_collection ) + if ( $_in_collection || $_in_taxonomy ) $data['meta']['links']['self'] = json_url( '/posts/types/' . $type->name ); else $data['meta']['links']['collection'] = json_url( '/posts/types' ); @@ -554,7 +555,7 @@ public function get_post_type( $type, $_in_collection = false ) { $data['meta']['links']['archives'] = json_url( add_query_arg( 'type', $type->name, '/posts' ) ); } - return apply_filters( 'json_post_type_data', $data, $type ); + return apply_filters( 'json_post_type_data', $data, $type, $_in_taxonomy ); } /** @@ -1089,6 +1090,22 @@ public function delete_meta( $id, $mid ) { return array( 'message' => __( 'Deleted meta' ) );; } + /** + * Embed post type data into taxonomy data + * + * @uses self::get_post_type() + * @param array $data Taxonomy data + * @param array $taxonomy Internal taxonomy data + * @return array Filtered data + */ + public function add_post_type_data( $data, $taxonomy ) { + foreach( $taxonomy->object_type as $type ) { + $data['types'][ $type ] = $this->get_post_type( $type, false, true ); + } + + return $data; + } + /** * Helper method for {@see new_post} and {@see edit_post}, containing shared logic. * diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index b05ca9e2e9..cc9b0acdcd 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -134,7 +134,7 @@ protected function prepare_taxonomy_object( $taxonomy, $_in_collection = false ) ), ); - return apply_filters( 'json_prepare_taxonomy', $data ); + return apply_filters( 'json_prepare_taxonomy', $data, $taxonomy ); } /** @@ -142,10 +142,13 @@ protected function prepare_taxonomy_object( $taxonomy, $_in_collection = false ) * * @param array $data Type data * @param array $post Internal type data + * @param boolean $_in_taxonomy The record being filtered is a taxonomy object (internal use) * @return array Filtered data */ - public function add_taxonomy_data( $data, $type ) { - $data['taxonomies'] = $this->get_taxonomies( $type->name ); + public function add_taxonomy_data( $data, $type, $_in_taxonomy = false ) { + if ( ! $_in_taxonomy ) { + $data['taxonomies'] = $this->get_taxonomies( $type->name ); + } return $data; } diff --git a/plugin.php b/plugin.php index 3571b043fc..82067b3bc3 100644 --- a/plugin.php +++ b/plugin.php @@ -75,6 +75,7 @@ function json_api_default_filters($server) { // Posts $wp_json_posts = new WP_JSON_Posts($server); add_filter( 'json_endpoints', array( $wp_json_posts, 'register_routes' ), 0 ); + add_filter( 'json_prepare_taxonomy', array( $wp_json_posts, 'add_post_type_data' ), 10, 2 ); // Users $wp_json_users = new WP_JSON_Users($server); @@ -97,7 +98,7 @@ function json_api_default_filters($server) { // Posts $wp_json_taxonomies = new WP_JSON_Taxonomies($server); add_filter( 'json_endpoints', array( $wp_json_taxonomies, 'register_routes' ), 2 ); - add_filter( 'json_post_type_data', array( $wp_json_taxonomies, 'add_taxonomy_data' ), 10, 2 ); + add_filter( 'json_post_type_data', array( $wp_json_taxonomies, 'add_taxonomy_data' ), 10, 3 ); add_filter( 'json_prepare_post', array( $wp_json_taxonomies, 'add_term_data' ), 10, 3 ); } add_action( 'wp_json_server_before_serve', 'json_api_default_filters', 10, 1 ); diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 4f8acd941f..202d9d57f9 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -126,4 +126,18 @@ public function test_get_taxonomy_term() { $this->assertEquals( $category->description, $data['description'] ); $this->assertEquals( $category->count, $data['count'] ); } + + public function test_add_taxonomy_data() { + // Mock type + $type = new stdClass; + $type->name = 'post'; + + // This record is not a taxonomy record: taxonomies should be embedded + $data = $this->endpoint->add_taxonomy_data( array(), $type, false ); + $this->assertArrayHasKey( 'taxonomies', $data ); + + // This record is a taxonomy record: taxonomies should NOT be embedded + $data_within_taxonomy = $this->endpoint->add_taxonomy_data( array(), $type, true ); + $this->assertArrayNotHasKey( 'taxonomies', $data_within_taxonomy ); + } } From ea482a834cf640636333cb9f01b2ab96d47ba1fb Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Wed, 28 May 2014 11:44:38 -0400 Subject: [PATCH 253/515] Allow customization of the API resources prefix. --- plugin.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/plugin.php b/plugin.php index 25f8001516..fc649f7323 100644 --- a/plugin.php +++ b/plugin.php @@ -46,8 +46,8 @@ function json_api_init() { add_action( 'init', 'json_api_init' ); function json_api_register_rewrites() { - add_rewrite_rule( '^wp-json/?$','index.php?json_route=/','top' ); - add_rewrite_rule( '^wp-json(.*)?','index.php?json_route=$matches[1]','top' ); + add_rewrite_rule( '^' . json_get_url_prefix() . '/?$','index.php?json_route=/','top' ); + add_rewrite_rule( '^' . json_get_url_prefix() . '(.*)?','index.php?json_route=$matches[1]','top' ); } /** @@ -352,6 +352,15 @@ function json_cookie_collect_status() { add_action( 'auth_cookie_bad_hash', 'json_cookie_collect_status' ); add_action( 'auth_cookie_valid', 'json_cookie_collect_status' ); +/** + * Get the URL prefix for any API resource. + * + * @return string Prefix. + */ +function json_get_url_prefix() { + return apply_filters( 'json_url_prefix', 'wp-json' ); +} + /** * Get URL to a JSON endpoint on a site * @@ -363,7 +372,7 @@ function json_cookie_collect_status() { */ function get_json_url( $blog_id = null, $path = '', $scheme = 'json' ) { if ( get_option( 'permalink_structure' ) ) { - $url = get_home_url( $blog_id, 'wp-json', $scheme ); + $url = get_home_url( $blog_id, json_get_url_prefix(), $scheme ); if ( ! empty( $path ) && is_string( $path ) && strpos( $path, '..' ) === false ) $url .= '/' . ltrim( $path, '/' ); From 3b3c069675b09f2d4276c43287d91ce48845a322 Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Thu, 29 May 2014 11:41:51 -0400 Subject: [PATCH 254/515] Return WP_Error instead of array of empty objects for a revisions permissions error --- lib/class-wp-json-posts.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index ce9ab12261..2503d6b8ba 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -88,6 +88,12 @@ public function get_revisions( $id ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } + $post = get_post( $id, ARRAY_A ); + + if ( ! $this->check_edit_permission( $post ) ) { + return new WP_Error( 'json_cannot_view', __( 'Sorry, you cannot view revisions.' ), array( 'status' => 403 ) ); + } + // Todo: Query args filter for wp_get_post_revisions $revisions = wp_get_post_revisions( $id ); From 9fd24ddde77e1839fc8d8ad63956d460163ce201 Mon Sep 17 00:00:00 2001 From: JDGrimes Date: Thu, 29 May 2014 17:09:36 -0400 Subject: [PATCH 255/515] Remove extra "which" in implementation doc --- docs/internals/implementation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/internals/implementation.md b/docs/internals/implementation.md index f5d93f3cd9..2dea658406 100644 --- a/docs/internals/implementation.md +++ b/docs/internals/implementation.md @@ -46,7 +46,7 @@ regarded as a bad API smell, as each parameter should be fully documented for consumers. It is possible to construct hacks around this using the `json_dispatch_args` filter, but this is intentionally made to feel hacky. -Some parameters which give information about the context of the call. These are +Some parameters give information about the context of the call. These are prefixed with an underscore, and are *always* set. This ensures that a rogue consumer can't pass in the internal name to override it and possibly cause a security issue. From 29760fd43b3f997207ac4aa65be65ca99aa01601 Mon Sep 17 00:00:00 2001 From: JDGrimes Date: Thu, 29 May 2014 17:14:12 -0400 Subject: [PATCH 256/515] Fix a copy/paste error in schema doc Change `date(_gmt)` to `modified(_gmt)` and rewrap. --- docs/schema.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/schema.md b/docs/schema.md index 51e895deb5..c29c9d829d 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -181,9 +181,9 @@ Section 5.6 datetime representation. [RFC3339]: http://tools.ietf.org/html/rfc3339 ### `modified`, `modified_gmt` -The `date` and `date_gmt` fields are strings with the post's last modification -date and time in the local time and UTC respectively. These fields follow the -[RFC3339][] Section 5.6 datetime representation. +The `modified` and `modified_gmt` fields are strings with the post's last +modification date and time in the local time and UTC respectively. These fields +follow the [RFC3339][] Section 5.6 datetime representation. modified = date-time modified_gmt = date-time From 3151df5b5e573ac30f21f3cf295b39560f8dc9a0 Mon Sep 17 00:00:00 2001 From: jeremyfelt Date: Fri, 30 May 2014 16:59:52 -0700 Subject: [PATCH 257/515] Set class-jsonserializable.php file mode to 644 --- lib/class-jsonserializable.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 lib/class-jsonserializable.php diff --git a/lib/class-jsonserializable.php b/lib/class-jsonserializable.php old mode 100755 new mode 100644 From 49de0835f8c7970782fa876722294c2652fa7da9 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sat, 31 May 2014 14:37:21 -0500 Subject: [PATCH 258/515] Minor style fixes to match coding standards. --- lib/class-wp-json-taxonomies.php | 2 ++ plugin.php | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 07e4ae5bfa..1aa11eecdf 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -34,6 +34,7 @@ public function register_routes( $routes ) { array( array( $this, 'get_taxonomy_term' ), WP_JSON_Server::READABLE ), ), ); + return array_merge( $routes, $tax_routes ); } @@ -197,6 +198,7 @@ public function get_taxonomy_terms( $taxonomy ) { foreach ( $terms as $term ) { $data[] = $this->prepare_taxonomy_term( $term ); } + return $data; } diff --git a/plugin.php b/plugin.php index 1009013412..e5747da219 100644 --- a/plugin.php +++ b/plugin.php @@ -78,7 +78,7 @@ function json_api_default_filters( $server ) { add_filter( 'json_prepare_taxonomy', array( $wp_json_posts, 'add_post_type_data' ), 10, 2 ); // Users - $wp_json_users = new WP_JSON_Users($server); + $wp_json_users = new WP_JSON_Users( $server ); add_filter( 'json_endpoints', array( $wp_json_users, 'register_routes' ), 0 ); add_filter( 'json_prepare_post', array( $wp_json_users, 'add_post_author_data' ), 10, 3 ); add_filter( 'json_prepare_comment', array( $wp_json_users, 'add_comment_author_data' ), 10, 3 ); @@ -96,7 +96,7 @@ function json_api_default_filters( $server ) { add_filter( 'json_post_type_data', array( $wp_json_media, 'type_archive_link' ), 10, 2 ); // Posts - $wp_json_taxonomies = new WP_JSON_Taxonomies($server); + $wp_json_taxonomies = new WP_JSON_Taxonomies( $server ); add_filter( 'json_endpoints', array( $wp_json_taxonomies, 'register_routes' ), 2 ); add_filter( 'json_post_type_data', array( $wp_json_taxonomies, 'add_taxonomy_data' ), 10, 3 ); add_filter( 'json_prepare_post', array( $wp_json_taxonomies, 'add_term_data' ), 10, 3 ); From e950f1a2d8fe9c226ae57bb0ae96daa0161201b0 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sat, 31 May 2014 16:45:52 -0500 Subject: [PATCH 259/515] Use tabs vs spaces in the test config file. --- phpunit.xml.dist | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 66723a23d1..73f87977e1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,8 +1,29 @@ - + tests + + + + + + + + . + + + ./lib + + \ No newline at end of file From 1965084978ddc43787dbc64c02f0565dbd837c53 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Sun, 1 Jun 2014 10:56:01 -0700 Subject: [PATCH 260/515] Corrected reference link --- docs/guides/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index b5d4050330..d7e4d321a3 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -238,7 +238,7 @@ And here's what we get back: }, "meta": { "links": { - "help": "https:\/\/github.com\/rmccue\/WP-API", + "help": "https:\/\/github.com\/WP-API\/WP-API", "profile": "https:\/\/raw.github.com\/rmccue\/WP-API\/master\/docs\/schema.json" } } From a830630c4575d3acc7d8cba2f5d2beccae7f586d Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Sun, 1 Jun 2014 11:55:46 -0700 Subject: [PATCH 261/515] Add missing post formats --- docs/schema.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/schema.md b/docs/schema.md index c29c9d829d..5ec00da414 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -334,7 +334,7 @@ indicates how some meta fields should be displayed. For example, posts with the "link" format may wish to display an extra link to a URL specified in a meta field or emphasise a link in the post content. - post-format = "standard" / "aside" / "gallery" / "image" / "link" / "status" + post-format = "standard" / "aside" / "gallery" / "image" / "link" / "status" / "quote" / "video" / "audio" / "chat" Providers MUST NOT use post formats not specified by this specification, unless specified in a subsequent version of the specification. Consumers MUST treat From ed33eb2f0fa95773d932fa834e20334f9371a844 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Sun, 1 Jun 2014 13:26:23 -0700 Subject: [PATCH 262/515] Make sure the type is actually evaluated --- lib/class-wp-json-customposttype.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-customposttype.php b/lib/class-wp-json-customposttype.php index ec2a0f5315..f82fb389de 100644 --- a/lib/class-wp-json-customposttype.php +++ b/lib/class-wp-json-customposttype.php @@ -210,7 +210,7 @@ protected function prepare_post( $post, $context = 'view' ) { $_post['meta']['links']['up'] = json_url( $this->base . '/' . $post['ID'] ); } - return apply_filters( 'json_prepare_{$this->type}', $_post, $post, $context ); + return apply_filters( "json_prepare_{$this->type}", $_post, $post, $context ); } /** From 922d58e32fd744e0236a2094d89552ba670540f5 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Sun, 1 Jun 2014 13:36:10 -0700 Subject: [PATCH 263/515] Make sure these methods are explicitly identified --- lib/class-wp-json-posts.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index ce9ab12261..15741440ae 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -285,7 +285,7 @@ protected function check_edit_permission( $post ) { * - any other fields supported by wp_insert_post() * @return array Post data (see {@see WP_JSON_Posts::get_post}) */ - function new_post( $data ) { + public function new_post( $data ) { unset( $data['ID'] ); $result = $this->insert_post( $data ); @@ -360,7 +360,7 @@ public function get_post( $id, $context = 'view' ) { * @param array $_headers Header data * @return true on success */ - function edit_post( $id, $data, $_headers = array() ) { + public function edit_post( $id, $data, $_headers = array() ) { $id = (int) $id; if ( empty( $id ) ) { From 10c7750826e3b04ae3cf7cdc0bc0d75afd5ac989 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Sun, 1 Jun 2014 13:54:49 -0700 Subject: [PATCH 264/515] get_post() safely handles $id, remove dupe error check --- lib/class-wp-json-posts.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index ce9ab12261..00c5abbdb3 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -311,13 +311,9 @@ function new_post( $data ) { public function get_post( $id, $context = 'view' ) { $id = (int) $id; - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { + if ( empty( $id ) || empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } From f35a0e30021ee796fd265ac059d6649d1ae33121 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 1 Jun 2014 19:18:06 -0500 Subject: [PATCH 265/515] Added test for get_revisions method. --- tests/test_json_posts.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index f533ee3d3f..4f90f7852f 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -371,6 +371,30 @@ function test_get_post() { $this->check_get_post_response( $response, $this->post_obj ); } + function test_get_revisions() { + wp_update_post( array( 'post_content' => 'This content is better.', 'ID' => $this->post_id ) ); + wp_update_post( array( 'post_content' => 'This content is marvelous.', 'ID' => $this->post_id ) ); + + $revisions = wp_get_post_revisions( $this->post_id ); + + $struct = array(); + foreach ( $revisions as $revision ) { + $post = get_object_vars( $revision ); + $struct[] = $post; + + $this->assertTrue( user_can( $this->author_id, 'edit_post', $revision->post_parent ) ); + } + + $response = $this->endpoint->get_revisions( $this->post_id ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + $response_data = $response->get_data(); + + // Check that we succeeded + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 2, $response_data ); + } + function test_edit_post() { $data = $this->set_data( array( 'ID' => $this->post_id ) ) ; $response = $this->endpoint->edit_post( $this->post_id, $data ); From 65690b89efafb36d647e0bac47ec67b3c18819ce Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Sun, 1 Jun 2014 19:24:09 -0500 Subject: [PATCH 266/515] Update all instances of duplicate $id checks --- lib/class-wp-json-posts.php | 42 +++++++------------------------------ 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 00c5abbdb3..cc48e46502 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -359,13 +359,9 @@ public function get_post( $id, $context = 'view' ) { function edit_post( $id, $data, $_headers = array() ) { $id = (int) $id; - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { + if ( empty( $id ) || empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -408,13 +404,9 @@ function edit_post( $id, $data, $_headers = array() ) { public function delete_post( $id, $force = false ) { $id = (int) $id; - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { + if ( empty( $id ) || empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -834,13 +826,9 @@ protected function prepare_excerpt( $excerpt ) { public function get_all_meta( $id ) { $id = (int) $id; - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { + if ( empty( $id ) || empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -877,13 +865,9 @@ public function get_all_meta( $id ) { public function get_meta( $id, $mid ) { $id = (int) $id; - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { + if ( empty( $id ) || empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -993,13 +977,9 @@ public function update_meta( $id, $mid, $data ) { $id = (int) $id; $mid = (int) $mid; - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { + if ( empty( $id ) || empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -1092,13 +1072,9 @@ protected function is_valid_meta_data( $data ) { public function add_meta( $id, $data ) { $id = (int) $id; - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { + if ( empty( $id ) || empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } @@ -1157,13 +1133,9 @@ public function add_meta( $id, $data ) { public function delete_meta( $id, $mid ) { $id = (int) $id; - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - $post = get_post( $id, ARRAY_A ); - if ( empty( $post['ID'] ) ) { + if ( empty( $id ) || empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } From bc69eadeff9c53eaf3c4f0fb292f1474273fa895 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 1 Jun 2014 20:03:10 -0500 Subject: [PATCH 267/515] Renamed permission check variable in get_revisions method to parent. --- lib/class-wp-json-posts.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index ed9292f2ce..0f227b4039 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -88,9 +88,9 @@ public function get_revisions( $id ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - $post = get_post( $id, ARRAY_A ); + $parent = get_post( $id, ARRAY_A ); - if ( ! $this->check_edit_permission( $post ) ) { + if ( ! $this->check_edit_permission( $parent ) ) { return new WP_Error( 'json_cannot_view', __( 'Sorry, you cannot view revisions.' ), array( 'status' => 403 ) ); } From 6301e803386ad518c8c95331b89eda72c0e382cf Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 1 Jun 2014 20:03:38 -0500 Subject: [PATCH 268/515] Added detail to permission check error message in get_revisions method. --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 0f227b4039..f073cb2fff 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -91,7 +91,7 @@ public function get_revisions( $id ) { $parent = get_post( $id, ARRAY_A ); if ( ! $this->check_edit_permission( $parent ) ) { - return new WP_Error( 'json_cannot_view', __( 'Sorry, you cannot view revisions.' ), array( 'status' => 403 ) ); + return new WP_Error( 'json_cannot_view', __( 'Sorry, you cannot view the revisions for this post.' ), array( 'status' => 403 ) ); } // Todo: Query args filter for wp_get_post_revisions From 36fcd2a30a3c6aa976dd43bb0638cff769e68549 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 1 Jun 2014 20:19:33 -0500 Subject: [PATCH 269/515] Return an empty array for posts without revisions. --- lib/class-wp-json-posts.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index f073cb2fff..8fa2039761 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -97,13 +97,18 @@ public function get_revisions( $id ) { // Todo: Query args filter for wp_get_post_revisions $revisions = wp_get_post_revisions( $id ); + $response = new WP_JSON_Response(); + + $struct = array(); foreach ( $revisions as $revision ) { $post = get_object_vars( $revision ); $struct[] = $this->prepare_post( $post, 'view-revision' ); } - return $struct; + $response->set_data( $struct ); + + return $response; } /** From 71975387a3b5929595f1d09dce85e8c30a307d8e Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 1 Jun 2014 20:25:27 -0500 Subject: [PATCH 270/515] Removed unneeded response object added in 36fcd2a. --- lib/class-wp-json-posts.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 8fa2039761..7551da40d8 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -97,8 +97,6 @@ public function get_revisions( $id ) { // Todo: Query args filter for wp_get_post_revisions $revisions = wp_get_post_revisions( $id ); - $response = new WP_JSON_Response(); - $struct = array(); foreach ( $revisions as $revision ) { $post = get_object_vars( $revision ); @@ -106,9 +104,7 @@ public function get_revisions( $id ) { $struct[] = $this->prepare_post( $post, 'view-revision' ); } - $response->set_data( $struct ); - - return $response; + return $struct; } /** From 841432b4aaae59ceaa32850d42fced815067399c Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 2 Jun 2014 11:32:21 +1000 Subject: [PATCH 271/515] Allow hijacking API requests --- lib/class-wp-json-server.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 3748b7f71a..5bdf14dcc6 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -227,6 +227,19 @@ public function serve_request( $path = null ) { $result = $this->check_authentication(); if ( ! is_wp_error( $result ) ) { + /** + * Allow hijacking the request before dispatching + * + * If `$result` is non-empty, this value will be used to serve the + * request instead. + * + * @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. + * @param WP_JSON_ResponseHandler $this ResponseHandler instance (usually WP_JSON_Server) + */ + $result = apply_filters( 'json_pre_dispatch', null, $this ); + } + + if ( empty( $result ) ) { $result = $this->dispatch(); } From 4d4db35fb5f80e7e039b7c9a2eb5d0cb6fdbe7d6 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 2 Jun 2014 11:32:50 +1000 Subject: [PATCH 272/515] Handle CORS and OPTIONS requests --- plugin.php | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/plugin.php b/plugin.php index 5e8719b5c9..518a2663b0 100644 --- a/plugin.php +++ b/plugin.php @@ -99,6 +99,10 @@ function json_api_default_filters( $server ) { add_filter( 'json_endpoints', array( $wp_json_taxonomies, 'register_routes' ), 2 ); add_filter( 'json_post_type_data', array( $wp_json_taxonomies, 'add_taxonomy_data' ), 10, 2 ); add_filter( 'json_prepare_post', array( $wp_json_taxonomies, 'add_term_data' ), 10, 3 ); + + // Default serving + add_filter( 'json_serve_request', 'json_send_cors_headers' ); + add_filter( 'json_pre_dispatch', 'json_handle_options_request', 10, 2 ); } add_action( 'wp_json_server_before_serve', 'json_api_default_filters', 10, 1 ); @@ -416,3 +420,64 @@ function json_ensure_response( $response ) { return new WP_JSON_Response( $response ); } + +/** + * Send Cross-Origin Resource Sharing headers with API requests + * + * @param mixed $value Response data + * @return mixed Response data + */ +function json_send_cors_headers( $value ) { + $origin = get_http_origin(); + + if ( $origin ) { + header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) ); + header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE' ); + header( 'Access-Control-Allow-Credentials: true' ); + } + + return $value; +} + +/** + * Handle OPTIONS requests for the server + * + * This is handled outside of the server code, as it doesn't obey normal route + * mapping. + * + * @param mixed $response Current response, either response or `null` to indicate pass-through + * @param WP_JSON_Server $handler ResponseHandler instance (usually WP_JSON_Server) + * @return WP_JSON_ResponseHandler Modified response, either response or `null` to indicate pass-through + */ +function json_handle_options_request( $response, $handler ) { + if ( ! empty( $response ) || $handler->method !== 'OPTIONS' ) { + return $response; + } + + $response = new WP_JSON_Response(); + + $accept = array(); + $map = $handler::$method_map; + foreach ( $handler->get_routes() as $route => $endpoints ) { + $match = preg_match( '@^' . $route . '$@i', $handler->path, $args ); + var_dump(array($route, $match)); + + if ( ! $match ) { + continue; + } + + foreach ( $endpoints as $endpoint ) { + foreach ( $map as $type => $bitmask ) { + if ( $endpoint[1] & $bitmask ) { + $accept[] = $type; + } + } + } + break; + } + $accept = array_unique( $accept ); + + $response->header( 'Accept', implode( ', ', $accept ) ); + + return $response; +} From 47d628d7145c25171da287818f16eb9c11964694 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 2 Jun 2014 14:29:59 +1000 Subject: [PATCH 273/515] Use correct URL for root JS URL --- plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.php b/plugin.php index 9b602f2bea..0dae4a5c2a 100644 --- a/plugin.php +++ b/plugin.php @@ -202,7 +202,7 @@ function json_api_deactivation( $network_wide ) { function json_register_scripts() { wp_register_script( 'wp-api', 'http://wp-api.github.io/client-js/build/js/wp-api.js', array( 'jquery', 'backbone', 'underscore' ), '1.1', true ); - $settings = array( 'root' => esc_url_raw( home_url( 'wp-json' ) ), 'nonce' => wp_create_nonce( 'wp_json' ) ); + $settings = array( 'root' => esc_url_raw( get_json_url() ), 'nonce' => wp_create_nonce( 'wp_json' ) ); wp_localize_script( 'wp-api', 'WP_API_Settings', $settings ); } add_action( 'wp_enqueue_scripts', 'json_register_scripts', -100 ); From a9f1dac205894625ed9ce7887ba717f4f3bd2750 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Sun, 1 Jun 2014 19:55:14 -0500 Subject: [PATCH 274/515] Test access to revisions --- lib/class-wp-json-posts.php | 12 +++---- tests/test-json-post-revisions.php | 54 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 tests/test-json-post-revisions.php diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 7551da40d8..c3c700b2ab 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -84,15 +84,15 @@ public function register_routes( $routes ) { public function get_revisions( $id ) { $id = (int) $id; - if ( empty( $id ) ) { + $post = get_post( $id, ARRAY_A ); + + if ( empty( $id ) || empty( $post['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - $parent = get_post( $id, ARRAY_A ); - - if ( ! $this->check_edit_permission( $parent ) ) { - return new WP_Error( 'json_cannot_view', __( 'Sorry, you cannot view the revisions for this post.' ), array( 'status' => 403 ) ); - } + if ( ! $this->check_edit_permission( $post ) ) { + return new WP_Error( 'json_cannot_view', __( 'Sorry, you cannot view the revisions for this post.' ), array( 'status' => 403 ) ); + } // Todo: Query args filter for wp_get_post_revisions $revisions = wp_get_post_revisions( $id ); diff --git a/tests/test-json-post-revisions.php b/tests/test-json-post-revisions.php new file mode 100644 index 0000000000..9da42c429c --- /dev/null +++ b/tests/test-json-post-revisions.php @@ -0,0 +1,54 @@ +author = $this->factory->user->create( array( 'user_role' => 'author' ) ); + $this->contributor = $this->factory->user->create( array( 'user_role' => 'contributor' ) ); + $this->post_id = $this->factory->post->create( array( + 'post_author' => $this->author, + 'post_title' => md5( wp_generate_password() ), + 'post_content' => md5( wp_generate_password() ) + ) ); + $this->revision_id = wp_save_post_revision( $this->post_id ); + + $this->fake_server = $this->getMock('WP_JSON_Server'); + $this->endpoint = new WP_JSON_Posts( $this->fake_server ); + + } + + public function test_revisions_access_by_user() { + + // Contributor shouldn't have access to another user's revisions + wp_set_current_user( $this->contributor ); + $response = $this->endpoint->get_revisions( $this->post_id ); + $this->assertInstanceOf( 'WP_Error', $response ); + $this->assertEquals( 403, $response->get_status() ); + + // Logged out users shouldn't have access to any revisions + wp_set_current_user( 0 ); + $response = $this->endpoint->get_revisions( $this->post_id ); + $this->assertInstanceOf( 'WP_Error', $response ); + $this->assertEquals( 403, $response->get_status() ); + + // Authors should have access to their own revisions + wp_set_current_user( $this->author ); + $response = $this->endpoint->get_revisions( $this->post_id ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $this->assertEquals( 200, $response->get_status() ); + $response = json_ensure_response( $response ); + $data = $response->get_data(); + $this->assertEquals( 1, count( $data ) ); + + } + +} \ No newline at end of file From 156d8d577479b5d1ef38146193f9825e0b6ca500 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Sun, 1 Jun 2014 20:04:26 -0500 Subject: [PATCH 275/515] Actually run our revisions tests, and use the error response helper --- phpunit.xml.dist | 1 + tests/test-json-post-revisions.php | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 66723a23d1..bc0db27c96 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,6 +3,7 @@ tests + tests \ No newline at end of file diff --git a/tests/test-json-post-revisions.php b/tests/test-json-post-revisions.php index 9da42c429c..0d6a5f022b 100644 --- a/tests/test-json-post-revisions.php +++ b/tests/test-json-post-revisions.php @@ -26,19 +26,28 @@ public function setUp() { } + protected function assertErrorResponse( $code, $response, $status = null ) { + $this->assertInstanceOf( 'WP_Error', $response ); + $this->assertEquals( $code, $response->get_error_code() ); + + if ( $status !== null ) { + $data = $response->get_error_data(); + $this->assertArrayHasKey( 'status', $data ); + $this->assertEquals( $status, $data['status'] ); + } + } + public function test_revisions_access_by_user() { // Contributor shouldn't have access to another user's revisions wp_set_current_user( $this->contributor ); $response = $this->endpoint->get_revisions( $this->post_id ); - $this->assertInstanceOf( 'WP_Error', $response ); - $this->assertEquals( 403, $response->get_status() ); + $this->assertErrorResponse( 'json_cannot_view', $response, 403 ); // Logged out users shouldn't have access to any revisions wp_set_current_user( 0 ); $response = $this->endpoint->get_revisions( $this->post_id ); - $this->assertInstanceOf( 'WP_Error', $response ); - $this->assertEquals( 403, $response->get_status() ); + $this->assertErrorResponse( 'json_cannot_view', $response, 403 ); // Authors should have access to their own revisions wp_set_current_user( $this->author ); From a83027d658caee3da6a32bfec017948ee3dca5a4 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Sun, 1 Jun 2014 20:12:20 -0500 Subject: [PATCH 276/515] Fix up tests --- tests/test-json-post-revisions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test-json-post-revisions.php b/tests/test-json-post-revisions.php index 0d6a5f022b..f2197b1364 100644 --- a/tests/test-json-post-revisions.php +++ b/tests/test-json-post-revisions.php @@ -12,8 +12,8 @@ class WP_Test_JSON_Post_Revisions extends WP_UnitTestCase { public function setUp() { parent::setUp(); - $this->author = $this->factory->user->create( array( 'user_role' => 'author' ) ); - $this->contributor = $this->factory->user->create( array( 'user_role' => 'contributor' ) ); + $this->author = $this->factory->user->create( array( 'role' => 'author' ) ); + $this->contributor = $this->factory->user->create( array( 'role' => 'contributor' ) ); $this->post_id = $this->factory->post->create( array( 'post_author' => $this->author, 'post_title' => md5( wp_generate_password() ), @@ -53,8 +53,8 @@ public function test_revisions_access_by_user() { wp_set_current_user( $this->author ); $response = $this->endpoint->get_revisions( $this->post_id ); $this->assertNotInstanceOf( 'WP_Error', $response ); - $this->assertEquals( 200, $response->get_status() ); $response = json_ensure_response( $response ); + $this->assertEquals( 200, $response->get_status() ); $data = $response->get_data(); $this->assertEquals( 1, count( $data ) ); From 1f8cb9d6ffc55b34be9d327b8186354b2a7cab8b Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Sun, 1 Jun 2014 20:18:40 -0500 Subject: [PATCH 277/515] Test that no revisions produces an empty response and not an error Fixes #227 --- tests/test-json-post-revisions.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test-json-post-revisions.php b/tests/test-json-post-revisions.php index f2197b1364..29465dea24 100644 --- a/tests/test-json-post-revisions.php +++ b/tests/test-json-post-revisions.php @@ -60,4 +60,22 @@ public function test_revisions_access_by_user() { } + public function test_post_no_revisions() { + + $no_revisions_id = $this->factory->post->create( array( + 'post_author' => $this->author, + 'post_title' => md5( wp_generate_password() ), + 'post_content' => md5( wp_generate_password() ) + ) ); + + wp_set_current_user( $this->author ); + $response = $this->endpoint->get_revisions( $no_revisions_id ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertEquals( 0, count( $data ) ); + + } + } \ No newline at end of file From a17e63536bc2fc8cac1464e7d5dc0fd39ad009b1 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 2 Jun 2014 14:43:44 +1000 Subject: [PATCH 278/515] Standardise revision test naming --- phpunit.xml.dist | 1 - ...test-json-post-revisions.php => test_json_post_revisions.php} | 0 2 files changed, 1 deletion(-) rename tests/{test-json-post-revisions.php => test_json_post_revisions.php} (100%) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bc0db27c96..66723a23d1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,7 +3,6 @@ tests - tests \ No newline at end of file diff --git a/tests/test-json-post-revisions.php b/tests/test_json_post_revisions.php similarity index 100% rename from tests/test-json-post-revisions.php rename to tests/test_json_post_revisions.php From b15efa3e59f5c340b93d85847358ac2de836c7dd Mon Sep 17 00:00:00 2001 From: pkevan Date: Mon, 2 Jun 2014 13:08:03 +0100 Subject: [PATCH 279/515] Add note to where upload_files cap comes from --- lib/class-wp-json-media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-media.php b/lib/class-wp-json-media.php index b6180fa97e..b139086c79 100644 --- a/lib/class-wp-json-media.php +++ b/lib/class-wp-json-media.php @@ -193,7 +193,7 @@ public function upload_attachment( $_files, $_headers ) { return new WP_Error( 'json_invalid_post_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); } - // Permissions check + // Permissions check - Note: "upload_files" cap is returned for an attachment by $post_type->cap->create_posts if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) { return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to post on this site.' ), array( 'status' => 400 ) ); } From 32f770b3ba49845cee82ec3552cdba9efe2cfcc0 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 2 Jun 2014 07:26:47 -0500 Subject: [PATCH 280/515] Rename variable in get_revisions method to $parent. Corrects regression in ebabff. --- lib/class-wp-json-posts.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index c3c700b2ab..ab3dd5faf6 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -84,13 +84,13 @@ public function register_routes( $routes ) { public function get_revisions( $id ) { $id = (int) $id; - $post = get_post( $id, ARRAY_A ); + $parent = get_post( $id, ARRAY_A ); - if ( empty( $id ) || empty( $post['ID'] ) ) { + if ( empty( $id ) || empty( $parent['ID'] ) ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! $this->check_edit_permission( $post ) ) { + if ( ! $this->check_edit_permission( $parent ) ) { return new WP_Error( 'json_cannot_view', __( 'Sorry, you cannot view the revisions for this post.' ), array( 'status' => 403 ) ); } From 66c7457850d92791665d913de0679915575008b3 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 2 Jun 2014 23:09:34 +1000 Subject: [PATCH 281/515] Test creating post with custom date --- tests/test_json_posts.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 4f90f7852f..e33aba7bab 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -357,6 +357,22 @@ function test_create_page_with_invalid_parent() { $this->assertErrorResponse( 'json_post_invalid_id', $response, 400 ); } + function test_create_post_custom_date() { + $data = $this->set_data(array( + 'date' => '2010-01-01T02:00:00Z', + )); + $time = gmmktime( 2, 0, 0, 1, 1, 2010 ); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + var_dump($new_post->post_date); + $this->assertEquals( $time, strtotime( $new_post->post_date ) ); + } + function test_get_post() { $response = $this->endpoint->get_post( $this->post_id ); From 3af845ed55812ba052d765d04638a281e1e113dd Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 2 Jun 2014 23:09:51 +1000 Subject: [PATCH 282/515] Don't mock out server methods This never should have been disabled, and was causing stupid errors. We can remove this when we refactor in #185. --- tests/test_json_posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index e33aba7bab..b1a1ba2e86 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -16,7 +16,7 @@ public function setUp() { $this->post_id = $this->factory->post->create(); $this->post_obj = get_post( $this->post_id ); - $this->fake_server = $this->getMock('WP_JSON_Server'); + $this->fake_server = $this->getMock( 'WP_JSON_Server', null ); $this->endpoint = new WP_JSON_Posts( $this->fake_server ); } From ab95e876a47d32be449f572feb80b7f5457012a0 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 2 Jun 2014 23:17:20 +1000 Subject: [PATCH 283/515] Test dates with custom TZs/GMT --- tests/test_json_posts.php | 49 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index b1a1ba2e86..44d46fdf9f 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -369,7 +369,54 @@ function test_create_post_custom_date() { $response_data = $response->get_data(); $new_post = get_post( $response_data['ID'] ); - var_dump($new_post->post_date); + $this->assertEquals( $time, strtotime( $new_post->post_date ) ); + } + + function test_create_post_custom_date_with_timezone() { + $data = $this->set_data(array( + 'date' => '2010-01-01T02:00:00-10:00', + )); + $time = gmmktime( 12, 0, 0, 1, 1, 2010 ); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $new_post->post_date ) ); + } + + function test_create_post_custom_date_gmt() { + $data = $this->set_data(array( + 'date_gmt' => '2010-01-01T02:00:00Z', + )); + $time = gmmktime( 2, 0, 0, 1, 1, 2010 ); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $new_post->post_date ) ); + } + + /** + * `date_gmt` fields should always be forced to UTC + */ + function test_create_post_custom_date_gmt_with_timezone() { + $data = $this->set_data(array( + 'date_gmt' => '2010-01-01T02:00:00+10:00', + )); + $time = gmmktime( 2, 0, 0, 1, 1, 2010 ); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); $this->assertEquals( $time, strtotime( $new_post->post_date ) ); } From 1e6b92df35ddc1c5ebb199a0e20e65cb685c521b Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 2 Jun 2014 23:32:13 +1000 Subject: [PATCH 284/515] Test that custom modified dates are ignored As per #285. --- lib/class-wp-json-posts.php | 3 +++ tests/test_json_posts.php | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 7a56bb741d..8b0a1d0296 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1199,6 +1199,8 @@ protected function insert_post( $data ) { } // Post modified + // See https://github.com/WP-API/WP-API/issues/285 + // @codeCoverageIgnoreStart if ( ! empty( $data['modified'] ) ) { $date_data = $this->server->get_date_with_gmt( $data['modified'] ); if ( ! empty( $date_data ) ) { @@ -1211,6 +1213,7 @@ protected function insert_post( $data ) { list( $post['post_modified'], $post['post_modified_gmt'] ) = $date_data; } } + // @codeCoverageIgnoreEnd // Post slug if ( ! empty( $data['name'] ) ) { diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 44d46fdf9f..2d19bf366f 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -420,6 +420,25 @@ function test_create_post_custom_date_gmt_with_timezone() { $this->assertEquals( $time, strtotime( $new_post->post_date ) ); } + /** + * Custom `modified` field values are ignored + */ + function test_create_post_custom_modified() { + $data = $this->set_data(array( + 'date' => '2010-01-01T02:00:00Z', + 'modified' => '2009-01-01T02:00:00Z', + )); + $time = gmmktime( 2, 0, 0, 1, 1, 2010 ); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $new_post->post_modified ) ); + } + function test_get_post() { $response = $this->endpoint->get_post( $this->post_id ); From 19a01ad4b76bce10b8beb477cf664bf874bde628 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 2 Jun 2014 23:38:58 +1000 Subject: [PATCH 285/515] Test menu order --- tests/test_json_posts.php | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 2d19bf366f..1e9bcccc80 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -439,6 +439,48 @@ function test_create_post_custom_modified() { $this->assertEquals( $time, strtotime( $new_post->post_modified ) ); } + function test_create_post_menu_order() { + $data = $this->set_data(array( + 'menu_order' => 5, + )); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['menu_order'], $new_post->menu_order ); + } + + function test_create_post_menu_order_negative() { + $data = $this->set_data(array( + 'menu_order' => -5, + )); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['menu_order'], $new_post->menu_order ); + } + + function test_create_post_menu_order_noninteger() { + $data = $this->set_data(array( + 'menu_order' => 'test', + )); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( 0, $new_post->menu_order ); + } + function test_get_post() { $response = $this->endpoint->get_post( $this->post_id ); From e03b13b83a678a0dd381cf0c6c106f2982580a28 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 2 Jun 2014 23:50:32 +1000 Subject: [PATCH 286/515] Test sticky posts --- tests/test_json_posts.php | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 1e9bcccc80..c384218ba0 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -481,6 +481,33 @@ function test_create_post_menu_order_noninteger() { $this->assertEquals( 0, $new_post->menu_order ); } + function test_create_post_sticky() { + $this->markTestSkipped('https://github.com/WP-API/WP-API/issues/218'); + $data = $this->set_data(array( + 'sticky' => true, + )); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $this->assertTrue( is_sticky( $response_data['ID'] ) ); + } + + function test_create_post_sticky_false() { + $data = $this->set_data(array( + 'sticky' => false, + )); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $this->assertFalse( is_sticky( $response_data['ID'] ) ); + } + function test_get_post() { $response = $this->endpoint->get_post( $this->post_id ); @@ -590,4 +617,33 @@ function test_edit_post_change_type_invalid() { $this->assertErrorResponse( 'json_invalid_post_type', $response, 400 ); } + function test_edit_post_sticky() { + $this->markTestSkipped('https://github.com/WP-API/WP-API/issues/218'); + $data = $this->set_data(array( + 'sticky' => true, + )); + + $response = $this->endpoint->edit_post( $this->post_id, $data ); + $response = json_ensure_response( $response ); + + $edited_post = get_post( $this->post_id ); + $this->check_get_post_response( $response, $edited_post ); + + $this->assertTrue( is_sticky( $this->post_id ) ); + } + + function test_edit_post_sticky_false() { + $data = $this->set_data(array( + 'sticky' => false, + )); + + $response = $this->endpoint->edit_post( $this->post_id, $data ); + $response = json_ensure_response( $response ); + + $edited_post = get_post( $this->post_id ); + $this->check_get_post_response( $response, $edited_post ); + + $this->assertFalse( is_sticky( $this->post_id ) ); + } + } From c4cb0a1f987b61e55368888eaa4054058227e504 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 00:00:09 +1000 Subject: [PATCH 287/515] Mark password test skipped --- tests/test_json_posts.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index c384218ba0..56c58bd55e 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -292,6 +292,7 @@ function test_create_post_publish_without_permission() { } function test_create_post_with_password() { + $this->markTestSkipped('https://github.com/WP-API/WP-API/issues/286'); $data = $this->set_data(array( 'password' => 'testing', )); From be57075bffc2f8e777cd8744ae2d3f80ec4efde6 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 00:02:59 +1000 Subject: [PATCH 288/515] Don't skip parent tests Now that #228 has been fixed, we can reenable these. --- tests/test_json_posts.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 56c58bd55e..ea42f4788b 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -323,8 +323,6 @@ function test_create_post_with_password_without_permission() { } function test_create_page_with_parent() { - $this->markTestSkipped('https://github.com/WP-API/WP-API/issues/228'); - $parent = $this->factory->post->create(array( 'type' => 'page', )); @@ -344,8 +342,6 @@ function test_create_page_with_parent() { } function test_create_page_with_invalid_parent() { - $this->markTestSkipped('https://github.com/WP-API/WP-API/issues/228'); - $parent = $this->factory->post->create(array( 'type' => 'page', )); From 385af45e95709b629fe5b488d35e588fa60b2935 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 00:04:21 +1000 Subject: [PATCH 289/515] Don't skip draft test We've now corrected the date issue in #229. Wa-hoo! --- tests/test_json_posts.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index ea42f4788b..475c471371 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -232,8 +232,6 @@ function test_create_post_without_permission() { } function test_create_post_draft() { - $this->markTestSkipped('https://github.com/WP-API/WP-API/issues/229'); - $data = $this->set_data(array( 'status' => 'draft', )); From 20401af126f69e96c028d4809ddbc32606197933 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 00:08:41 +1000 Subject: [PATCH 290/515] Test invalid statuses --- tests/test_json_posts.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 475c471371..9df9943d20 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -289,6 +289,23 @@ function test_create_post_publish_without_permission() { $this->assertErrorResponse( 'json_cannot_publish', $response, 403 ); } + /** + * Invalid statuses should be defaulted to "draft" status + */ + function test_create_post_invalid_status() { + $data = $this->set_data(array( + 'status' => 'teststatus', + )); + + $response = $this->endpoint->new_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( 'draft', $new_post->post_status ); + } + function test_create_post_with_password() { $this->markTestSkipped('https://github.com/WP-API/WP-API/issues/286'); $data = $this->set_data(array( From fce8d31f5d11c129bf125e6b8f86d215725597b2 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 2 Jun 2014 10:54:02 -0500 Subject: [PATCH 291/515] Update sticky post handling to allow setting when creating or editing posts. --- lib/class-wp-json-posts.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 3312777bb2..ede268645a 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1463,12 +1463,13 @@ protected function insert_post( $data ) { } // Sticky - if ( isset( $post['sticky'] ) ) { - if ( $post['sticky'] ) { - stick_post( $data['ID'] ); - } else { - unstick_post( $data['ID'] ); - } + if ( isset( $data['sticky'] ) ) { + $post['sticky'] = $data['sticky']; + } + if ( $post['sticky'] ) { + stick_post( $post_ID ); + } else { + unstick_post( $post_ID ); } do_action( 'json_insert_post', $post, $data, $update ); From 8bbf3631090d33f2f5537af560b33fbb4ad93799 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 10:46:01 +1000 Subject: [PATCH 292/515] Remove modified date code in insert_post Turns out WP doesn't actually let us set this. Damn. --- lib/class-wp-json-posts.php | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 3312777bb2..005bfdf963 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1329,24 +1329,6 @@ protected function insert_post( $data ) { } } - // Post modified - // See https://github.com/WP-API/WP-API/issues/285 - // @codeCoverageIgnoreStart - if ( ! empty( $data['modified'] ) ) { - $date_data = $this->server->get_date_with_gmt( $data['modified'] ); - - if ( ! empty( $date_data ) ) { - list( $post['post_modified'], $post['post_modified_gmt'] ) = $date_data; - } - } elseif ( ! empty( $data['modified_gmt'] ) ) { - $date_data = $this->server->get_date_with_gmt( $data['modified_gmt'], true ); - - if ( ! empty( $date_data ) ) { - list( $post['post_modified'], $post['post_modified_gmt'] ) = $date_data; - } - } - // @codeCoverageIgnoreEnd - // Post slug if ( ! empty( $data['name'] ) ) { $post['post_name'] = $data['name']; From a9a762b58f2df5c09fbac9057587379ef2c27bf2 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 2 Jun 2014 19:59:24 -0500 Subject: [PATCH 293/515] Moved stick and unstick post conditional inside check of data['sticky'] --- lib/class-wp-json-posts.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index ede268645a..6ed7942115 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1464,12 +1464,11 @@ protected function insert_post( $data ) { // Sticky if ( isset( $data['sticky'] ) ) { - $post['sticky'] = $data['sticky']; - } - if ( $post['sticky'] ) { - stick_post( $post_ID ); - } else { - unstick_post( $post_ID ); + if ( $data['sticky'] ) { + stick_post( $post_ID ); + } else { + unstick_post( $post_ID ); + } } do_action( 'json_insert_post', $post, $data, $update ); From b010af6514b80f5d7609f97f54b7bcecbcba48aa Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 2 Jun 2014 20:09:36 -0500 Subject: [PATCH 294/515] Don't skip test_create_post_sticky anymore. --- tests/test_json_posts.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 9df9943d20..03540f65db 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -494,7 +494,6 @@ function test_create_post_menu_order_noninteger() { } function test_create_post_sticky() { - $this->markTestSkipped('https://github.com/WP-API/WP-API/issues/218'); $data = $this->set_data(array( 'sticky' => true, )); From b3e9947552cf824486c337fe443469304431fcd9 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 2 Jun 2014 20:14:56 -0500 Subject: [PATCH 295/515] Added sticky parameters to post route docs for creating and editing posts. --- docs/routes/posts.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index 1099aabb4d..318e3b91d3 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -69,6 +69,8 @@ pairs: * `ping_status` - Ping status for the post: `open` or `closed`. Indicates whether users can submit pingbacks or trackbacks to the post. Default is the option 'default_ping_status'. (string) *optional* +* `sticky` - Sticky status for the post: `true` or `false`. Default is + `false`. (boolean) *optional* ### Response @@ -215,6 +217,8 @@ pairs: * `ping_status` - Ping status for the post: `open` or `closed`. Indicates whether users can submit pingbacks or trackbacks to the post. Default is the option 'default_ping_status'. (string) *optional* +* `sticky` - Sticky status for the post: `true` or `false`. Default is + `false`. (boolean) *optional* ### Response From f6f594da668f0f9ac6d4d3a218e8561da244e345 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 2 Jun 2014 20:44:26 -0500 Subject: [PATCH 296/515] Removed modified date parameters from create and edit routes docs. --- docs/routes/posts.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index 1099aabb4d..c7cd16ecb7 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -44,14 +44,6 @@ pairs: Date should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). Example: 201401-01T12:20:52Z. Default is the current GMT date and time. (string) *optional* -* `modified` - Date and time the post was last modified in local time. Date - should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). - Example: 201401-01T12:20:52Z. Default is the local date and time. (string) - *optional* -* `modified_gmt` - Date and time the post was last modified in UTC time. Date - should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). - Example: 201401-01T12:20:52Z. Default is the current GMT date and time. - (string) *optional* * `author` - Author of the post. Author can be provided as a string of the author's ID or as the User object of the author. Default is current user. (object \| string) *optional* @@ -190,14 +182,6 @@ pairs: Date should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). Example: 201401-01T12:20:52Z. Default is the current GMT date and time. (string) *optional* -* `modified` - Date and time the post was last modified in local time. Date - should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). - Example: 201401-01T12:20:52Z. Default is the local date and time. (string) - *optional* -* `modified_gmt` - Date and time the post was last modified in UTC time. Date - should be an [RFC3339 timestamp](http://tools.ietf.org/html/rfc3339). - Example: 201401-01T12:20:52Z. Default is the current GMT date and time. - (string) *optional* * `author` - Author of the post. Author can be provided as a string of the author's ID or as the User object of the author. Default is current user. (object \| string) *optional* From f82d13b4c9292259108c5aade7c59d2b880166b7 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 12:05:49 +1000 Subject: [PATCH 297/515] Access method map in PHP 5.2-compatible way --- plugin.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugin.php b/plugin.php index 518a2663b0..bef4e4f8e5 100644 --- a/plugin.php +++ b/plugin.php @@ -457,7 +457,11 @@ function json_handle_options_request( $response, $handler ) { $response = new WP_JSON_Response(); $accept = array(); - $map = $handler::$method_map; + + $handler_class = get_class( $handler ); + $class_vars = get_class_vars( $handler_class ); + $map = $class_vars['method_map']; + foreach ( $handler->get_routes() as $route => $endpoints ) { $match = preg_match( '@^' . $route . '$@i', $handler->path, $args ); var_dump(array($route, $match)); From 7e44c58a6d720df59e309da06ec2febdb365a9fd Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 12:06:43 +1000 Subject: [PATCH 298/515] Remove var_dump --- plugin.php | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin.php b/plugin.php index bef4e4f8e5..0c8baff4fb 100644 --- a/plugin.php +++ b/plugin.php @@ -464,7 +464,6 @@ function json_handle_options_request( $response, $handler ) { foreach ( $handler->get_routes() as $route => $endpoints ) { $match = preg_match( '@^' . $route . '$@i', $handler->path, $args ); - var_dump(array($route, $match)); if ( ! $match ) { continue; From db02e312cc849e130dd032f316c669ae2482cfc4 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 14:06:09 +1000 Subject: [PATCH 299/515] Hide deprecated routes from index --- lib/class-wp-json-taxonomies.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 1aa11eecdf..29fc1535c3 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -13,13 +13,13 @@ public function register_routes( $routes ) { array( array( $this, 'get_taxonomies' ), WP_JSON_Server::READABLE ), ), '/posts/types/(?P\w+)/taxonomies/(?P\w+)' => array( - array( array( $this, 'get_taxonomy' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_taxonomy' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), ), '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms' => array( - array( array( $this, 'get_terms' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_terms' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), ), '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms/(?P\w+)' => array( - array( array( $this, 'get_term' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_term' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), ), '/taxonomies' => array( array( array( $this, 'get_taxonomies' ), WP_JSON_Server::READABLE ), From ff19d84f6b6a135b0d6e35653cce00dd1e39ff0e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 14:06:23 +1000 Subject: [PATCH 300/515] Mark legacy routes as deprecated --- lib/class-wp-json-taxonomies.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 29fc1535c3..f4384326c2 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -76,6 +76,8 @@ public function get_taxonomies( $type = null ) { * @return array Taxonomy data */ public function get_taxonomy( $type, $taxonomy ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, 'WPAPI-1.1', __CLASS__ . '::get_taxonomy_object' ); + return $this->get_taxonomy_object( $taxonomy ); } @@ -107,6 +109,8 @@ public function get_taxonomy_object( $taxonomy ) { * @return array Taxonomy data */ protected function prepare_taxonomy( $taxonomy, $type = null, $_in_collection = false ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, 'WPAPI-1.1', __CLASS__ . '::prepare_taxonomy_object' ); + return $this->prepare_taxonomy_object( $taxonomy, $_in_collection ); } @@ -170,6 +174,8 @@ public function add_taxonomy_data( $data, $type, $_in_taxonomy = false ) { * @return array Term collection */ public function get_terms( $type, $taxonomy ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, 'WPAPI-1.1', __CLASS__ . '::get_taxonomy_terms' ); + return $this->get_taxonomy_terms( $taxonomy ); } From 815635f05d6f1a5a391f7ec5b4581adf056550dd Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 14:06:49 +1000 Subject: [PATCH 301/515] Pass deprecated errors via headers --- plugin.php | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/plugin.php b/plugin.php index e5747da219..c5ba8b9704 100644 --- a/plugin.php +++ b/plugin.php @@ -100,6 +100,12 @@ function json_api_default_filters( $server ) { add_filter( 'json_endpoints', array( $wp_json_taxonomies, 'register_routes' ), 2 ); add_filter( 'json_post_type_data', array( $wp_json_taxonomies, 'add_taxonomy_data' ), 10, 3 ); add_filter( 'json_prepare_post', array( $wp_json_taxonomies, 'add_term_data' ), 10, 3 ); + + // Deprecated reporting + add_action( 'deprecated_function_run', 'json_handle_deprecated_function', 10, 3 ); + add_filter( 'deprecated_function_trigger_error', '__return_false' ); + add_action( 'deprecated_argument_run', 'json_handle_deprecated_argument', 10, 3 ); + add_filter( 'deprecated_argument_trigger_error', '__return_false' ); } add_action( 'wp_json_server_before_serve', 'json_api_default_filters', 10, 1 ); @@ -419,3 +425,38 @@ function json_ensure_response( $response ) { return new WP_JSON_Response( $response ); } + +/** + * Handle `_deprecated_function` errors + * + * @param string $function + * @param string $replacement + * @param string $version + */ +function json_handle_deprecated_function( $function, $replacement, $version ) { + if ( ! empty( $replacement ) ) { + $string = sprintf( __('%1$s (since %2$s; use %3$s instead)'), $function, $version, $replacement ); + } + else { + $string = sprintf( __('%1$s (since %2$s; no alternative available)'), $function, $version ); + } + + header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) ); +} +/** + * Handle `_deprecated_function` errors + * + * @param string $function + * @param string $replacement + * @param string $version + */ +function json_handle_deprecated_argument( $function, $message, $version ) { + if ( ! empty( $message ) ) { + $string = sprintf( __('%1$s (since %2$s; %3$s)'), $function, $version, $message ); + } + else { + $string = sprintf( __('%1$s (since %2$s; no alternative available)'), $function, $version ); + } + + header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) ); +} From b4acfcaacd4eb737dbc78fd9eee4a8881960a9da Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 14:09:33 +1000 Subject: [PATCH 302/515] Add missing whitespace --- plugin.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin.php b/plugin.php index c5ba8b9704..fbaf679dc1 100644 --- a/plugin.php +++ b/plugin.php @@ -443,6 +443,7 @@ function json_handle_deprecated_function( $function, $replacement, $version ) { header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) ); } + /** * Handle `_deprecated_function` errors * From 90376f63459e8bc67f8979f29f3e197046068e0e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 14:24:32 +1000 Subject: [PATCH 303/515] Test for missing parameters These currently fail, but shouldn't. Huzzah! --- tests/test_json_users.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_json_users.php b/tests/test_json_users.php index 1877978cf6..6fa9c05a17 100644 --- a/tests/test_json_users.php +++ b/tests/test_json_users.php @@ -91,6 +91,15 @@ public function test_create_user() { $this->assertTrue( wp_check_password( $data['password'], $new_user->user_pass ), 'Password check failed' ); } + public function test_create_user_missing_params() { + $this->user_obj->set_role( 'administrator' ); + $data = array( + 'username' => 'test_user', + ); + $response = $this->endpoint->new_user( $data ); + $this->assertInstanceOf( 'WP_Error', $response ); + } + public function test_delete_user() { $this->user_obj->set_role( 'administrator' ); @@ -140,4 +149,26 @@ public function test_delete_user_reassign() { $post = get_post( $test_post ); $this->assertEquals( $test_new_author, $post->post_author ); } + + public function test_update_user() { + $data = array( + 'first_name' => 'New Name', + ); + $response = $this->endpoint->edit_user( $this->user, $data ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + + if ( ! $response instanceof WP_JSON_ResponseInterface ) { + $response = new WP_JSON_Response( $response ); + } + + // Check that we succeeded + $this->assertEquals( 200, $response->get_status() ); + + // Check that the name has been updated correctly + $new_data = $response->get_data(); + $this->assertEquals( $data['first_name'], $new_data['first_name'] ); + + $user = get_userdata( $this->user ); + $this->assertEquals( $user->first_name, $data['first_name'] ); + } } From 15bb5a01d48c7f1a6d6a3e2374869cb377422f62 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 14:41:36 +1000 Subject: [PATCH 304/515] Check parameters in the correct place Update doesn't require the user data, creation does. --- lib/class-wp-json-users.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 85d7d019d8..b0de391ec9 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -234,14 +234,6 @@ protected function insert_user( $data ) { return new WP_Error( 'json_user_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => 403 ) ); } - $required = array( 'username', 'password', 'email' ); - - foreach ( $required as $arg ) { - if ( empty( $data[ $arg ] ) ) { - return new WP_Error( 'json_missing_callback_param', sprintf( __( 'Missing parameter %s' ), $arg ), array( 'status' => 400 ) ); - } - } - $update = true; } else { $user = new WP_User(); @@ -253,6 +245,14 @@ protected function insert_user( $data ) { return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to create users.' ), array( 'status' => 403 ) ); } + $required = array( 'username', 'password', 'email' ); + + foreach ( $required as $arg ) { + if ( empty( $data[ $arg ] ) ) { + return new WP_Error( 'json_missing_callback_param', sprintf( __( 'Missing parameter %s' ), $arg ), array( 'status' => 400 ) ); + } + } + $update = false; } From 2ea6448297724d89e09e7d92f7aa1c50982e2f99 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 14:42:05 +1000 Subject: [PATCH 305/515] Work around WP data handling failure --- lib/class-wp-json-users.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index b0de391ec9..2419433fc8 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -223,10 +223,12 @@ public function add_comment_author_data( $data, $comment, $context ) { } protected function insert_user( $data ) { + $user = new stdClass; + if ( ! empty( $data['ID'] ) ) { - $user = get_userdata( $data['ID'] ); + $existing = get_userdata( $data['ID'] ); - if ( ! $user ) { + if ( ! $existing ) { return new WP_Error( 'json_user_invalid_id', __( 'Invalid user ID.' ), array( 'status' => 404 ) ); } @@ -234,13 +236,9 @@ protected function insert_user( $data ) { return new WP_Error( 'json_user_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => 403 ) ); } + $user->ID = $existing->ID; $update = true; } else { - $user = new WP_User(); - - // Workaround for https://core.trac.wordpress.org/ticket/28019 - $user->data = new stdClass; - if ( ! current_user_can( 'create_users' ) ) { return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to create users.' ), array( 'status' => 403 ) ); } From ef77fdee123b3a2c1cb50381706b17cbc538e3c2 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jun 2014 15:01:31 +1000 Subject: [PATCH 306/515] Check that we're not mangling user passwords --- tests/test_json_users.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_json_users.php b/tests/test_json_users.php index 6fa9c05a17..df5fdcb5e6 100644 --- a/tests/test_json_users.php +++ b/tests/test_json_users.php @@ -151,6 +151,8 @@ public function test_delete_user_reassign() { } public function test_update_user() { + $pw_before = $this->user_obj->user_pass; + $data = array( 'first_name' => 'New Name', ); @@ -170,5 +172,9 @@ public function test_update_user() { $user = get_userdata( $this->user ); $this->assertEquals( $user->first_name, $data['first_name'] ); + + // Check that we haven't inadvertently changed the user's password, + // as per https://core.trac.wordpress.org/ticket/21429 + $this->assertEquals( $pw_before, $user->user_pass ); } } From 91a816aa4aceb13f7cf2de4ec1cfd1a77aab7fb4 Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Tue, 3 Jun 2014 16:11:15 -0700 Subject: [PATCH 307/515] Docs audit - plugin.php. --- plugin.php | 123 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 75 insertions(+), 48 deletions(-) diff --git a/plugin.php b/plugin.php index 0dae4a5c2a..1322b43e83 100644 --- a/plugin.php +++ b/plugin.php @@ -9,14 +9,14 @@ */ /** - * Version number for our API + * Version number for our API. * * @var string */ define( 'JSON_API_VERSION', '1.0' ); /** - * Include our files for the API + * Include our files for the API. */ include_once( dirname( __FILE__ ) . '/lib/class-jsonserializable.php' ); @@ -45,6 +45,9 @@ function json_api_init() { } add_action( 'init', 'json_api_init' ); +/** + * Add rewrite rules. + */ function json_api_register_rewrites() { add_rewrite_rule( '^' . json_get_url_prefix() . '/?$','index.php?json_route=/','top' ); add_rewrite_rule( '^' . json_get_url_prefix() . '(.*)?','index.php?json_route=$matches[1]','top' ); @@ -65,28 +68,28 @@ function json_api_maybe_flush_rewrites() { add_action( 'init', 'json_api_maybe_flush_rewrites', 999 ); /** - * Register the default JSON API filters + * Register the default JSON API filters. * * @internal This will live in default-filters.php */ function json_api_default_filters( $server ) { global $wp_json_posts, $wp_json_pages, $wp_json_media, $wp_json_taxonomies; - // Posts + // Posts. $wp_json_posts = new WP_JSON_Posts( $server ); add_filter( 'json_endpoints', array( $wp_json_posts, 'register_routes' ), 0 ); - // Users + // Users. $wp_json_users = new WP_JSON_Users($server); add_filter( 'json_endpoints', array( $wp_json_users, 'register_routes' ), 0 ); add_filter( 'json_prepare_post', array( $wp_json_users, 'add_post_author_data' ), 10, 3 ); add_filter( 'json_prepare_comment', array( $wp_json_users, 'add_comment_author_data' ), 10, 3 ); - // Pages + // Pages. $wp_json_pages = new WP_JSON_Pages( $server ); $wp_json_pages->register_filters(); - // Media + // Media. $wp_json_media = new WP_JSON_Media( $server ); add_filter( 'json_endpoints', array( $wp_json_media, 'register_routes' ), 1 ); add_filter( 'json_prepare_post', array( $wp_json_media, 'add_thumbnail_data' ), 10, 3 ); @@ -94,7 +97,7 @@ function json_api_default_filters( $server ) { add_filter( 'json_insert_post', array( $wp_json_media, 'attach_thumbnail' ), 10, 3 ); add_filter( 'json_post_type_data', array( $wp_json_media, 'type_archive_link' ), 10, 2 ); - // Posts + // Posts. $wp_json_taxonomies = new WP_JSON_Taxonomies($server); add_filter( 'json_endpoints', array( $wp_json_taxonomies, 'register_routes' ), 2 ); add_filter( 'json_post_type_data', array( $wp_json_taxonomies, 'add_taxonomy_data' ), 10, 2 ); @@ -103,7 +106,7 @@ function json_api_default_filters( $server ) { add_action( 'wp_json_server_before_serve', 'json_api_default_filters', 10, 1 ); /** - * Load the JSON API + * Load the JSON API. * * @todo Extract code that should be unit tested into isolated methods such as * the wp_json_server_class filter and serving requests. This would also @@ -115,7 +118,7 @@ function json_api_loaded() { return; /** - * Whether this is a XMLRPC Request + * Whether this is a XML-RPC Request. * * @var bool * @todo Remove me in favour of JSON_REQUEST @@ -123,7 +126,7 @@ function json_api_loaded() { define( 'XMLRPC_REQUEST', true ); /** - * Whether this is a JSON Request + * Whether this is a JSON Request. * * @var bool */ @@ -142,20 +145,22 @@ function json_api_loaded() { * action rather than another action to ensure they're only loaded when * needed. * - * @param WP_JSON_ResponseHandler $wp_json_server Response handler object + * @param WP_JSON_ResponseHandler $wp_json_server Response handler object. */ do_action( 'wp_json_server_before_serve', $wp_json_server ); // Fire off the request $wp_json_server->serve_request( $GLOBALS['wp']->query_vars['json_route'] ); - // Finish off our request + // Finish off our request. die(); } add_action( 'template_redirect', 'json_api_loaded', -100 ); /** * Register routes and flush the rewrite rules on activation. + * + * @param bool $network_wide ? */ function json_api_activation( $network_wide ) { if ( function_exists( 'is_multisite' ) && is_multisite() && $network_wide ) { @@ -177,7 +182,9 @@ function json_api_activation( $network_wide ) { register_activation_hook( __FILE__, 'json_api_activation' ); /** - * Flush the rewrite rules on deactivation + * Flush the rewrite rules on deactivation. + * + * @param bool $network_wide ? */ function json_api_deactivation( $network_wide ) { if ( function_exists( 'is_multisite' ) && is_multisite() && $network_wide ) { @@ -250,26 +257,26 @@ function json_output_link_header() { add_action( 'template_redirect', 'json_output_link_header', 11, 0 ); /** - * Add `show_in_json` {@see register_post_type} argument + * Add `show_in_json` {@see register_post_type} argument. * - * Adds the `show_in_json` post type argument to {@see register_post_type}. This - * value controls whether the post type is available via API endpoints, and - * defaults to the value of `publicly_queryable` + * Adds the `show_in_json` post type argument to {@see register_post_type}. + * This value controls whether the post type is available via API endpoints, + * and defaults to the value of `publicly_queryable` * - * @param string $post_type Post type being registered - * @param stdClass $args Post type arguments + * @param string $post_type Post type to register. + * @param stdClass $args Post type arguments. */ function json_register_post_type( $post_type, $args ) { global $wp_post_types; $type = &$wp_post_types[ $post_type ]; - // Exception for pages + // Exception for pages. if ( $post_type === 'page' ) { $type->show_in_json = true; } - // Exception for revisions + // Exception for revisions. if ( $post_type === 'revision' ) { $type->show_in_json = true; } @@ -281,13 +288,15 @@ function json_register_post_type( $post_type, $args ) { add_action( 'registered_post_type', 'json_register_post_type', 10, 2 ); /** - * Check for errors when using cookie-based authentication + * Check for errors when using cookie-based authentication. * - * WordPress' built-in cookie authentication is always active for logged in - * users. However, the API has to check nonces for each request to ensure users - * are not vulnerable to CSRF. + * WordPress' built-in cookie authentication is always active + * for logged in users. However, the API has to check nonces + * for each request to ensure users are not vulnerable to CSRF. * - * @param WP_Error|mixed $result Error from another authentication handler, null if we should handle it, or another value if not + * @param WP_Error|mixed $result Error from another authentication handler, + * null if we should handle it, or another + * value if not * @return WP_Error|mixed|boolean */ function json_cookie_check_errors( $result ) { @@ -297,9 +306,11 @@ function json_cookie_check_errors( $result ) { global $wp_json_auth_cookie; - // Are we using cookie authentication? - // (If we get an auth error, but we're still logged in, another - // authentication must have been used.) + /* + * Are we using cookie authentication? (If we get an auth + * error, but we're still logged in, another authentication + * must have been used.) + */ if ( $wp_json_auth_cookie !== true && is_user_logged_in() ) { return $result; } @@ -313,12 +324,12 @@ function json_cookie_check_errors( $result ) { } if ( $nonce === null ) { - // No nonce at all, so act as if it's an unauthenticated request + // No nonce at all, so act as if it's an unauthenticated request. wp_set_current_user( 0 ); return true; } - // Check the nonce + // Check the nonce. $result = wp_verify_nonce( $nonce, 'wp_json' ); if ( ! $result ) { return new WP_Error( 'json_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) ); @@ -329,12 +340,10 @@ function json_cookie_check_errors( $result ) { add_filter( 'json_authentication_errors', 'json_cookie_check_errors', 100 ); /** - * Collect cookie authentication status - * - * Collects errors from {@see wp_validate_auth_cookie} for use by - * {@see json_cookie_check_errors}. + * Collect cookie authentication status. * - * @param mixed + * Collects errors from {@see wp_validate_auth_cookie} for + * use by {@see json_cookie_check_errors}. */ function json_cookie_collect_status() { global $wp_json_auth_cookie; @@ -360,17 +369,25 @@ function json_cookie_collect_status() { * @return string Prefix. */ function json_get_url_prefix() { + /** + * Filter the JSON URL prefix. + * + * @since 1.0 + * + * @param string $prefix URL prefix. + */ return apply_filters( 'json_url_prefix', 'wp-json' ); } /** - * Get URL to a JSON endpoint on a site + * Get URL to a JSON endpoint on a site. * * @todo Check if this is even necessary - * @param int $blog_id Blog ID - * @param string $path JSON route - * @param string $scheme Sanitization scheme (usually 'json') - * @return string Full URL to the endpoint + * + * @param int $blog_id Blog ID. + * @param string $path Optional. JSON route. Default empty. + * @param string $scheme Optional. Sanitization scheme. Default 'json'. + * @return string Full URL to the endpoint. */ function get_json_url( $blog_id = null, $path = '', $scheme = 'json' ) { if ( get_option( 'permalink_structure' ) ) { @@ -390,22 +407,32 @@ function get_json_url( $blog_id = null, $path = '', $scheme = 'json' ) { $url = add_query_arg( 'json_route', $path, $url ); } + /** + * Filter the JSON URL. + * + * @since 1.0 + * + * @param string $url JSON URL. + * @param string $path JSON route. + * @param int $blod_ig Blog ID. + * @param string $scheme Sanitization scheme. + */ return apply_filters( 'json_url', $url, $path, $blog_id, $scheme ); } /** - * Get URL to a JSON endpoint + * Get URL to a JSON endpoint. * - * @param string $path JSON route - * @param string $scheme Sanitization scheme (usually 'json') - * @return string Full URL to the endpoint + * @param string $path Optional. JSON route. Default empty. + * @param string $scheme Optional. Sanitization scheme. Default 'json'. + * @return string Full URL to the endpoint. */ function json_url( $path = '', $scheme = 'json' ) { return get_json_url( null, $path, $scheme ); } /** - * Ensure a JSON response is a response object + * Ensure a JSON response is a response object. * * This ensures that the response is consistent, and implements * {@see WP_JSON_ResponseInterface}, allowing usage of @@ -413,7 +440,7 @@ function json_url( $path = '', $scheme = 'json' ) { * also allow {@see WP_Error} to indicate error responses, so users should * immediately check for this value. * - * @param WP_Error|WP_JSON_ResponseInterface|mixed $response Response to check + * @param WP_Error|WP_JSON_ResponseInterface|mixed $response Response to check. * @return WP_Error|WP_JSON_ResponseInterface */ function json_ensure_response( $response ) { From 493c7025fa41a3308a70e8e25b499ec61dae1920 Mon Sep 17 00:00:00 2001 From: Paul Kevan Date: Thu, 5 Jun 2014 14:36:59 +0000 Subject: [PATCH 308/515] Allows adding of post id through post vars to allow image to be associated with post --- lib/class-wp-json-media.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-json-media.php b/lib/class-wp-json-media.php index b139086c79..57c3d3562f 100644 --- a/lib/class-wp-json-media.php +++ b/lib/class-wp-json-media.php @@ -186,8 +186,13 @@ public function delete_post( $id, $force = false ) { * @param array $_headers HTTP headers from the request * @return array|WP_Error Attachment data or error */ - public function upload_attachment( $_files, $_headers ) { + public function upload_attachment( $_files, $_headers, $post_id = 0 ) { + $post_type = get_post_type_object( 'attachment' ); + $post_type_post = get_post_type_object( 'post' ); + + // Make sure we have an int or 0 + $post_id = (int) $post_id; if ( ! $post_type ) { return new WP_Error( 'json_invalid_post_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); @@ -198,6 +203,11 @@ public function upload_attachment( $_files, $_headers ) { return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to post on this site.' ), array( 'status' => 400 ) ); } + // If a user is trying to attach to a post make sure they have permissions. Bail early if post_id is not being passed + if ( $post_id !== 0 && ! current_user_can( $post_type_post->cap->edit_post, $post_id ) ) { + return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => 401 ) ); + } + // Get the file via $_FILES or raw data if ( empty( $_files ) ) { $file = $this->upload_from_data( $_files, $_headers ); @@ -235,7 +245,7 @@ public function upload_attachment( $_files, $_headers ) { $attachment = array( 'post_mime_type' => $type, 'guid' => $url, - // 'post_parent' => $post_id, + 'post_parent' => $post_id, 'post_title' => $title, 'post_content' => $content, ); @@ -246,7 +256,7 @@ public function upload_attachment( $_files, $_headers ) { } // Save the data - $id = wp_insert_attachment($attachment, $file /*, $post_id */); + $id = wp_insert_attachment($attachment, $file, $post_id ); if ( !is_wp_error($id) ) { wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) ); From 1c727ffe2b9d39180376056b531dacff986da975 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 21:05:58 +1000 Subject: [PATCH 309/515] Change _in_collection to context Allows view/embed rather than a boolean, but keeps backwards compatibility by deprecating. --- lib/class-wp-json-taxonomies.php | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index f4384326c2..bd60d812ea 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -44,7 +44,7 @@ public function register_routes( $routes ) { * @param string|null $type A specific post type for which to retrieve taxonomies (optional) * @return array Taxonomy data */ - public function get_taxonomies( $type = null ) { + public function get_taxonomies( $type = null, $context = 'view' ) { if ( null === $type ) { $taxonomies = get_taxonomies( '', 'objects' ); } else { @@ -54,7 +54,7 @@ public function get_taxonomies( $type = null ) { $data = array(); foreach ( $taxonomies as $tax_type => $value ) { - $tax = $this->prepare_taxonomy_object( $value, true ); + $tax = $this->prepare_taxonomy_object( $value, $context ); if ( is_wp_error( $tax ) ) { continue; } @@ -105,27 +105,37 @@ public function get_taxonomy_object( $taxonomy ) { * * @param stdClass $taxonomy Taxonomy data * @param string|null $type Post type to get taxonomies for (deprecated) - * @param boolean $_in_collection Are we in a collection? + * @param string $context Context (view|embed). True/false are allowed for backwards compatibility, and map to embed/view respectively. * @return array Taxonomy data */ - protected function prepare_taxonomy( $taxonomy, $type = null, $_in_collection = false ) { + protected function prepare_taxonomy( $taxonomy, $type = null, $context = 'view' ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, 'WPAPI-1.1', __CLASS__ . '::prepare_taxonomy_object' ); - return $this->prepare_taxonomy_object( $taxonomy, $_in_collection ); + return $this->prepare_taxonomy_object( $taxonomy, $context ); } /** * Prepare a taxonomy object for serialization * * @param stdClass $taxonomy Taxonomy data - * @param boolean $_in_collection Are we in a collection? + * @param string $context Context (view|embed). True/false are allowed for backwards compatibility, and map to embed/view respectively. * @return array Taxonomy data */ - protected function prepare_taxonomy_object( $taxonomy, $_in_collection = false ) { + protected function prepare_taxonomy_object( $taxonomy, $context = 'view' ) { if ( $taxonomy->public === false ) { return new WP_Error( 'json_cannot_read_taxonomy', __( 'Cannot view taxonomy' ), array( 'status' => 403 ) ); } + // Backwards compatibility with _in_collection parameter + if ( $context === true ) { + $context = 'embed'; + _deprecated_argument( __CLASS__ . '::' . __FUNCTION__, 'WPAPI-1.1', '$context should be set to "embed" rather than true' ); + } + elseif ( $context === false ) { + $context = 'view'; + _deprecated_argument( __CLASS__ . '::' . __FUNCTION__, 'WPAPI-1.1', '$context should be set to "view" rather than false' ); + } + $base_url = '/taxonomies/' . $taxonomy->name; $data = array( @@ -144,7 +154,7 @@ protected function prepare_taxonomy_object( $taxonomy, $_in_collection = false ) ), ); - return apply_filters( 'json_prepare_taxonomy', $data, $taxonomy ); + return apply_filters( 'json_prepare_taxonomy', $data, $taxonomy, $context ); } /** From 30772e0a012d06c81439de01edf963175c33538d Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 21:08:30 +1000 Subject: [PATCH 310/515] Set context to embed for type taxonomies --- lib/class-wp-json-posts.php | 9 ++++++--- lib/class-wp-json-taxonomies.php | 2 +- plugin.php | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 0718eeb9eb..ff56226e17 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1208,11 +1208,14 @@ public function delete_meta( $id, $mid ) { * @uses self::get_post_type() * @param array $data Taxonomy data * @param array $taxonomy Internal taxonomy data + * @param string $context Context (view|embed) * @return array Filtered data */ - public function add_post_type_data( $data, $taxonomy ) { - foreach( $taxonomy->object_type as $type ) { - $data['types'][ $type ] = $this->get_post_type( $type, false, true ); + public function add_post_type_data( $data, $taxonomy, $context = 'view' ) { + if ( $context !== 'embed' ) { + foreach( $taxonomy->object_type as $type ) { + $data['types'][ $type ] = $this->get_post_type( $type, false, true ); + } } return $data; diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index bd60d812ea..9619287586 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -167,7 +167,7 @@ protected function prepare_taxonomy_object( $taxonomy, $context = 'view' ) { */ public function add_taxonomy_data( $data, $type, $_in_taxonomy = false ) { if ( ! $_in_taxonomy ) { - $data['taxonomies'] = $this->get_taxonomies( $type->name ); + $data['taxonomies'] = $this->get_taxonomies( $type->name, 'embed' ); } return $data; diff --git a/plugin.php b/plugin.php index fbaf679dc1..3091d4b172 100644 --- a/plugin.php +++ b/plugin.php @@ -75,7 +75,7 @@ function json_api_default_filters( $server ) { // Posts $wp_json_posts = new WP_JSON_Posts( $server ); add_filter( 'json_endpoints', array( $wp_json_posts, 'register_routes' ), 0 ); - add_filter( 'json_prepare_taxonomy', array( $wp_json_posts, 'add_post_type_data' ), 10, 2 ); + add_filter( 'json_prepare_taxonomy', array( $wp_json_posts, 'add_post_type_data' ), 10, 3 ); // Users $wp_json_users = new WP_JSON_Users( $server ); From e379b9924081944a4abf1575759c40c223855e9f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 21:10:04 +1000 Subject: [PATCH 311/515] Use an array of type strings for embed context --- lib/class-wp-json-posts.php | 1 + lib/class-wp-json-taxonomies.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index ff56226e17..4043894731 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1213,6 +1213,7 @@ public function delete_meta( $id, $mid ) { */ public function add_post_type_data( $data, $taxonomy, $context = 'view' ) { if ( $context !== 'embed' ) { + $data['types'] = array(); foreach( $taxonomy->object_type as $type ) { $data['types'][ $type ] = $this->get_post_type( $type, false, true ); } diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 9619287586..df3d35104b 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -142,7 +142,7 @@ protected function prepare_taxonomy_object( $taxonomy, $context = 'view' ) { 'name' => $taxonomy->label, 'slug' => $taxonomy->name, 'labels' => $taxonomy->labels, - 'types' => array(), + 'types' => $taxonomy->object_type, 'show_cloud' => $taxonomy->show_tagcloud, 'hierarchical' => $taxonomy->hierarchical, 'meta' => array( From 46d24bdaef4310439b6c6f9703e76d42150d7671 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 21:49:54 +1000 Subject: [PATCH 312/515] Add taxonomies link for post types This requires a custom relation, as there isn't an IANA standard relation for this. --- lib/class-wp-json-posts.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 4043894731..9ff4c12c3c 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -588,6 +588,12 @@ public function get_post_type( $type, $_in_collection = false, $_in_taxonomy = f $data['meta']['links']['collection'] = json_url( '/posts/types' ); } + // Add taxonomy link + $relation = 'http://wp-api.org/1.1/collections/taxonomy/'; + $url = json_url( '/taxonomies' ); + $url = add_query_arg( 'type', $type->name, $url ); + $data['meta']['links'][ $relation ] = $url; + if ( $type->publicly_queryable ) { if ( $type->name === 'post' ) { $data['meta']['links']['archives'] = json_url( '/posts' ); From 38a46ed16990e7e33a691fd870efca8527ca483b Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 21:52:35 +1000 Subject: [PATCH 313/515] Deprecate /posts/types//taxonomies --- lib/class-wp-json-taxonomies.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index df3d35104b..cb544bcebb 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -10,7 +10,7 @@ class WP_JSON_Taxonomies { public function register_routes( $routes ) { $tax_routes = array( '/posts/types/(?P\w+)/taxonomies' => array( - array( array( $this, 'get_taxonomies' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_taxonomies_for_type' ), WP_JSON_Server::READABLE ), ), '/posts/types/(?P\w+)/taxonomies/(?P\w+)' => array( array( array( $this, 'get_taxonomy' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), @@ -38,6 +38,18 @@ public function register_routes( $routes ) { return array_merge( $routes, $tax_routes ); } + /** + * Get taxonomies for a post type + * @param string $type Post type + * @param string $context Context (view|embed) + * @return array Taxonomy data + */ + public function get_taxonomies_for_type( $type, $context = 'view' ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, 'WPAPI-1.1', __CLASS__ . '::get_taxonomies( $type )' ); + + return $this->get_taxonomies( $type, $context ); + } + /** * Get taxonomies * From 95527dabb97e5b27ad0453bf30800fea42822d89 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 22:14:24 +1000 Subject: [PATCH 314/515] Check deprecated get_taxonomy response --- tests/test_json_taxonomies.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 202d9d57f9..5681f7bb11 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -6,7 +6,7 @@ * @package WordPress * @subpackage JSON API */ -class WP_Test_JSON_Taxonomies extends WP_UnitTestCase { +class WP_Test_JSON_Taxonomies extends WP_Test_JSON_TestCase { /** * This function is run before each method */ @@ -71,8 +71,7 @@ public function test_get_taxonomies_with_types() { } } - public function test_get_taxonomy_object() { - $response = $this->endpoint->get_taxonomy_object( 'category' ); + protected function check_taxonomy_object_response( $response ) { $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); @@ -87,6 +86,19 @@ public function test_get_taxonomy_object() { $this->assertEquals( $category->hierarchical, $data['hierarchical'] ); } + /** + * @expectedDeprecated WP_JSON_Taxonomies::get_taxonomy + */ + public function test_get_taxonomy() { + $response = $this->endpoint->get_taxonomy( 'post', 'category' ); + $this->check_taxonomy_object_response( $response ); + } + + public function test_get_taxonomy_object() { + $response = $this->endpoint->get_taxonomy_object( 'category' ); + $this->check_taxonomy_object_response( $response ); + } + public function test_get_taxonomy_terms() { $response = $this->endpoint->get_taxonomy_terms( 'category' ); $this->assertNotInstanceOf( 'WP_Error', $response ); From 614a65648f25fdb2347bb05b353b067a616e50c9 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 22:14:44 +1000 Subject: [PATCH 315/515] Check deprecated get_taxonomies_for_type response --- tests/test_json_taxonomies.php | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 5681f7bb11..7bfbe96311 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -35,6 +35,27 @@ private function get_public_taxonomies( $taxonomies ) { return array_values( array_filter( $taxonomies, array( $this, 'is_public' ) ) ); } + protected function check_taxonomies_for_type_response( $type, $response ) { + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + + $taxonomies = $this->get_public_taxonomies( get_object_taxonomies( $type, 'objects' ) ); + + $this->assertEquals( count( $taxonomies ), count( $data ) ); + } + + /** + * @expectedDeprecated WP_JSON_Taxonomies::get_taxonomies_for_type + */ + public function test_get_taxonomies_for_type() { + $response = $this->endpoint->get_taxonomies_for_type( 'post' ); + $this->check_taxonomies_for_type_response( 'post', $response ); + } + public function test_get_taxonomies() { $response = $this->endpoint->get_taxonomies(); $this->assertNotInstanceOf( 'WP_Error', $response ); @@ -58,16 +79,7 @@ public function test_get_taxonomies() { public function test_get_taxonomies_with_types() { foreach ( get_post_types() as $type ) { $response = $this->endpoint->get_taxonomies( $type ); - $this->assertNotInstanceOf( 'WP_Error', $response ); - $response = json_ensure_response( $response ); - - $this->assertEquals( 200, $response->get_status() ); - - $data = $response->get_data(); - - $taxonomies = $this->get_public_taxonomies( get_object_taxonomies( $type, 'objects' ) ); - - $this->assertEquals( count( $taxonomies ), count( $data ) ); + $this->check_taxonomies_for_type_response( $type, $response ); } } From 452fe20f8f39435eb0e26e764a53fc919f670847 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 22:24:21 +1000 Subject: [PATCH 316/515] Check other deprecated methods --- tests/test_json_taxonomies.php | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 7bfbe96311..1cead46888 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -111,8 +111,12 @@ public function test_get_taxonomy_object() { $this->check_taxonomy_object_response( $response ); } - public function test_get_taxonomy_terms() { - $response = $this->endpoint->get_taxonomy_terms( 'category' ); + public function test_get_taxonomy_object_empty() { + $response = $this->endpoint->get_taxonomy_object( '' ); + $this->assertErrorResponse( 'json_taxonomy_invalid_id', $response, 404 ); + } + + protected function check_get_taxonomy_terms_response( $response ) { $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); @@ -134,8 +138,20 @@ public function test_get_taxonomy_terms() { $this->assertEquals( $categories[0]->count, $data[0]['count']); } - public function test_get_taxonomy_term() { - $response = $this->endpoint->get_taxonomy_term( 'category', 1 ); + /** + * @expectedDeprecated WP_JSON_Taxonomies::get_terms + */ + public function test_get_terms() { + $response = $this->endpoint->get_terms( 'post', 'category' ); + $this->check_get_taxonomy_terms_response( $response ); + } + + public function test_get_taxonomy_terms() { + $response = $this->endpoint->get_taxonomy_terms( 'category' ); + $this->check_get_taxonomy_terms_response( $response ); + } + + public function check_get_taxonomy_term_response( $response ) { $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); From 8127e18c9bc297a1760905554018504319f5d6a7 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 22:24:35 +1000 Subject: [PATCH 317/515] Test routes are registered --- tests/test_json_taxonomies.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 1cead46888..0a12d6863e 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -20,6 +20,20 @@ public function setUp() { $this->endpoint = new WP_JSON_Taxonomies( $this->fake_server ); } + public function test_register_routes() { + $routes = array(); + $routes = $this->endpoint->register_routes( $routes ); + + $this->assertArrayHasKey( '/posts/types/(?P\w+)/taxonomies', $routes ); + $this->assertArrayHasKey( '/posts/types/(?P\w+)/taxonomies/(?P\w+)', $routes ); + $this->assertArrayHasKey( '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms', $routes ); + $this->assertArrayHasKey( '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms/(?P\w+)', $routes ); + $this->assertArrayHasKey( '/taxonomies', $routes ); + $this->assertArrayHasKey( '/taxonomies/(?P\w+)', $routes ); + $this->assertArrayHasKey( '/taxonomies/(?P\w+)/terms', $routes ); + $this->assertArrayHasKey( '/taxonomies/(?P\w+)/terms/(?P\w+)', $routes ); + } + /** * Utility function for use in get_public_taxonomies */ From 88768665fc854cd9ccaadfc78c8fa0d3dd4069b1 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 22:25:14 +1000 Subject: [PATCH 318/515] Test term response --- tests/test_json_taxonomies.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 0a12d6863e..3f26b78a95 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -181,6 +181,16 @@ public function check_get_taxonomy_term_response( $response ) { $this->assertEquals( $category->count, $data['count'] ); } + public function test_get_term() { + $response = $this->endpoint->get_term( 'post', 'category', 1 ); + $this->check_get_taxonomy_term_response( $response ); + } + + public function test_get_taxonomy_term() { + $response = $this->endpoint->get_taxonomy_term( 'category', 1 ); + $this->check_get_taxonomy_term_response( $response ); + } + public function test_add_taxonomy_data() { // Mock type $type = new stdClass; From 65592e2e0d5fb752085265c0d1eb082890d09ee5 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 22:36:50 +1000 Subject: [PATCH 319/515] Hide /posts/types//taxonomies --- lib/class-wp-json-taxonomies.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index cb544bcebb..ae7542a3d1 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -10,7 +10,7 @@ class WP_JSON_Taxonomies { public function register_routes( $routes ) { $tax_routes = array( '/posts/types/(?P\w+)/taxonomies' => array( - array( array( $this, 'get_taxonomies_for_type' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_taxonomies_for_type' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), ), '/posts/types/(?P\w+)/taxonomies/(?P\w+)' => array( array( array( $this, 'get_taxonomy' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), From ef013a1b742191657f2f5243b8215f672a08cb27 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 22:38:12 +1000 Subject: [PATCH 320/515] Check deprecated routes for hidden status --- tests/test_json_taxonomies.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 3f26b78a95..5194e22a18 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -24,14 +24,25 @@ public function test_register_routes() { $routes = array(); $routes = $this->endpoint->register_routes( $routes ); - $this->assertArrayHasKey( '/posts/types/(?P\w+)/taxonomies', $routes ); - $this->assertArrayHasKey( '/posts/types/(?P\w+)/taxonomies/(?P\w+)', $routes ); - $this->assertArrayHasKey( '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms', $routes ); - $this->assertArrayHasKey( '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms/(?P\w+)', $routes ); $this->assertArrayHasKey( '/taxonomies', $routes ); $this->assertArrayHasKey( '/taxonomies/(?P\w+)', $routes ); $this->assertArrayHasKey( '/taxonomies/(?P\w+)/terms', $routes ); $this->assertArrayHasKey( '/taxonomies/(?P\w+)/terms/(?P\w+)', $routes ); + + $deprecated = array( + '/posts/types/(?P\w+)/taxonomies', + '/posts/types/(?P\w+)/taxonomies/(?P\w+)', + '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms', + '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms/(?P\w+)', + ); + + foreach ( $deprecated as $route ) { + $this->assertArrayHasKey( $route, $routes ); + foreach ( $routes[$route] as $parts ) { + $bitmask = $parts[1]; + $this->assertEquals( WP_JSON_Server::HIDDEN_ENDPOINT, $bitmask & WP_JSON_Server::HIDDEN_ENDPOINT, "Deprecated $route should be hidden" ); + } + } } /** From a4015209760dc494a88505c8617a8c0bfe3e02a1 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 23:04:43 +1000 Subject: [PATCH 321/515] Test preparation methods --- tests/test_json_taxonomies.php | 56 +++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 5194e22a18..ea899b869a 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -108,6 +108,13 @@ public function test_get_taxonomies_with_types() { } } + protected function check_taxonomy_object( $tax_obj, $data ) { + $this->assertEquals( $tax_obj->label, $data['name'] ); + $this->assertEquals( $tax_obj->name, $data['slug'] ); + $this->assertEquals( $tax_obj->show_tagcloud, $data['show_cloud'] ); + $this->assertEquals( $tax_obj->hierarchical, $data['hierarchical'] ); + } + protected function check_taxonomy_object_response( $response ) { $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); @@ -117,10 +124,7 @@ protected function check_taxonomy_object_response( $response ) { $data = $response->get_data(); $category = get_taxonomy( 'category' ); - $this->assertEquals( $category->label, $data['name'] ); - $this->assertEquals( $category->name, $data['slug'] ); - $this->assertEquals( $category->show_tagcloud, $data['show_cloud'] ); - $this->assertEquals( $category->hierarchical, $data['hierarchical'] ); + $this->check_taxonomy_object( $category, $data ); } /** @@ -141,6 +145,50 @@ public function test_get_taxonomy_object_empty() { $this->assertErrorResponse( 'json_taxonomy_invalid_id', $response, 404 ); } + protected function call_protected( $method, $args ) { + $endpoint_class = new ReflectionClass( 'WP_JSON_Taxonomies' ); + $method = $endpoint_class->getMethod( $method ); + $method->setAccessible( true ); + $endpoint = $endpoint_class->newInstance(); + + return $method->invokeArgs( $endpoint, $args ); + } + + /** + * @expectedDeprecated WP_JSON_Taxonomies::prepare_taxonomy + */ + public function test_prepare_taxonomy() { + $tax = get_taxonomy( 'category' ); + $data = $this->call_protected( 'prepare_taxonomy', array( $tax ) ); + $this->check_taxonomy_object( $tax, $data ); + } + + public function test_prepare_taxonomy_object() { + $tax = get_taxonomy( 'category' ); + $data = $this->call_protected( 'prepare_taxonomy_object', array( $tax ) ); + $this->check_taxonomy_object( $tax, $data ); + } + + /** + * Check old _in_collection parameter + * @expectedDeprecated WP_JSON_Taxonomies::prepare_taxonomy_object + */ + public function test_prepare_taxonomy_object_in_collection() { + $tax = get_taxonomy( 'category' ); + $data = $this->call_protected( 'prepare_taxonomy_object', array( $tax, true ) ); + $this->check_taxonomy_object( $tax, $data ); + } + + /** + * Check old _in_collection parameter + * @expectedDeprecated WP_JSON_Taxonomies::prepare_taxonomy_object + */ + public function test_prepare_taxonomy_object_not_in_collection() { + $tax = get_taxonomy( 'category' ); + $data = $this->call_protected( 'prepare_taxonomy_object', array( $tax, false ) ); + $this->check_taxonomy_object( $tax, $data ); + } + protected function check_get_taxonomy_terms_response( $response ) { $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); From 14a77c9ca25c1f3dffcca3b70743bec814ffea49 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 23:13:14 +1000 Subject: [PATCH 322/515] Check get_taxonomy_terms errors --- tests/test_json_taxonomies.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index ea899b869a..108d05f20a 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -224,6 +224,29 @@ public function test_get_taxonomy_terms() { $this->check_get_taxonomy_terms_response( $response ); } + public function test_get_taxonomy_terms_empty() { + $response = $this->endpoint->get_taxonomy_terms( '' ); + $this->assertErrorResponse( 'json_taxonomy_invalid_id', $response, 404 ); + } + + public function test_get_taxonomy_terms_invalid() { + $response = $this->endpoint->get_taxonomy_terms( 'testtaxonomy' ); + $this->assertErrorResponse( 'json_taxonomy_invalid_id', $response, 404 ); + } + + public function get_terms_return_error() { + return new WP_Error( 'test_internal_error' ); + } + + public function test_get_taxonomy_terms_internal_error() { + add_filter( 'get_terms', array( $this, 'get_terms_return_error' ) ); + + $response = $this->endpoint->get_taxonomy_terms( 'category' ); + remove_filter( 'get_terms', array( $this, 'get_terms_return_error' ) ); + + $this->assertErrorResponse( 'test_internal_error', $response ); + } + public function check_get_taxonomy_term_response( $response ) { $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); From 0d3636bd56aed78891ae95aa3ab902fcf3ff52ed Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 23:21:04 +1000 Subject: [PATCH 323/515] Check get_taxonomy_term errors --- tests/test_json_taxonomies.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 108d05f20a..e6eda7a876 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -273,6 +273,16 @@ public function test_get_taxonomy_term() { $this->check_get_taxonomy_term_response( $response ); } + public function test_get_taxonomy_term_invalid_taxonomy() { + $response = $this->endpoint->get_taxonomy_term( 'testtaxonomy', 1 ); + $this->assertErrorResponse( 'json_taxonomy_invalid_id', $response, 404 ); + } + + public function test_get_taxonomy_term_invalid_term() { + $response = $this->endpoint->get_taxonomy_term( 'category', 'testmissingcat' ); + $this->assertErrorResponse( 'json_taxonomy_invalid_term', $response, 404 ); + } + public function test_add_taxonomy_data() { // Mock type $type = new stdClass; From 8197511754336fb6e9b954a54ac0f939b6f24503 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 23:34:54 +1000 Subject: [PATCH 324/515] Fire deprecated notices for get_term/prepare_term --- lib/class-wp-json-taxonomies.php | 4 ++++ tests/test_json_taxonomies.php | 3 +++ 2 files changed, 7 insertions(+) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index ae7542a3d1..44e0a11ea5 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -243,6 +243,8 @@ public function get_taxonomy_terms( $taxonomy ) { * @return array Term entity */ public function get_term( $type, $taxonomy, $term, $context = 'view' ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, 'WPAPI-1.1', __CLASS__ . '::get_taxonomy_term( $taxonomy, $term )' ); + return $this->get_taxonomy_term( $taxonomy, $term, $context ); } @@ -299,6 +301,8 @@ public function add_term_data( $data, $post, $context ) { * @return array The prepared term data */ protected function prepare_term( $term, $type = null, $context = 'view' ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, 'WPAPI-1.1', __CLASS__ . '::prepare_taxonomy_term( $term )' ); + return $this->prepare_taxonomy_term( $term, $context ); } diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index e6eda7a876..1545117573 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -263,6 +263,9 @@ public function check_get_taxonomy_term_response( $response ) { $this->assertEquals( $category->count, $data['count'] ); } + /** + * @expectedDeprecated WP_JSON_Taxonomies::get_term + */ public function test_get_term() { $response = $this->endpoint->get_term( 'post', 'category', 1 ); $this->check_get_taxonomy_term_response( $response ); From 138a0d92224c69d83514baeac7f2b515b5f9fa30 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 23:37:05 +1000 Subject: [PATCH 325/515] Check errors for prepare_taxonomy_term --- tests/test_json_taxonomies.php | 45 +++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 1545117573..3a56240d25 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -247,6 +247,14 @@ public function test_get_taxonomy_terms_internal_error() { $this->assertErrorResponse( 'test_internal_error', $response ); } + protected function check_taxonomy_term( $term, $data ) { + $this->assertEquals( $term->term_id, $data['ID'] ); + $this->assertEquals( $term->name, $data['name'] ); + $this->assertEquals( $term->slug, $data['slug'] ); + $this->assertEquals( $term->description, $data['description'] ); + $this->assertEquals( $term->count, $data['count'] ); + } + public function check_get_taxonomy_term_response( $response ) { $this->assertNotInstanceOf( 'WP_Error', $response ); $response = json_ensure_response( $response ); @@ -255,12 +263,7 @@ public function check_get_taxonomy_term_response( $response ) { $data = $response->get_data(); $category = get_term( 1, 'category' ); - - $this->assertEquals( $category->term_id, $data['ID'] ); - $this->assertEquals( $category->name, $data['name'] ); - $this->assertEquals( $category->slug, $data['slug'] ); - $this->assertEquals( $category->description, $data['description'] ); - $this->assertEquals( $category->count, $data['count'] ); + $this->check_taxonomy_term( $category, $data ); } /** @@ -299,4 +302,34 @@ public function test_add_taxonomy_data() { $data_within_taxonomy = $this->endpoint->add_taxonomy_data( array(), $type, true ); $this->assertArrayNotHasKey( 'taxonomies', $data_within_taxonomy ); } + + public function test_prepare_taxonomy_term() { + $term = get_term( 1, 'category' ); + $data = $this->call_protected( 'prepare_taxonomy_term', array( $term ) ); + $this->check_taxonomy_term( $term, $data ); + } + + public function test_prepare_taxonomy_term_child() { + $child = $this->factory->category->create( array( + 'parent' => 1, + ) ); + + $term = get_term( $child, 'category' ); + $data = $this->call_protected( 'prepare_taxonomy_term', array( $term ) ); + $this->check_taxonomy_term( $term, $data ); + + $this->assertNotEmpty( $data['parent'] ); + + $parent = get_term( 1, 'category' ); + $this->check_taxonomy_term( $parent, $data['parent'] ); + } + + /** + * @expectedDeprecated WP_JSON_Taxonomies::prepare_term + */ + public function test_prepare_term() { + $term = get_term( 1, 'category' ); + $data = $this->call_protected( 'prepare_term', array( $term, 'post' ) ); + $this->check_taxonomy_term( $term, $data ); + } } From 4b91cbd6bbbad6405a02950c5f491795566c7fb3 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 8 Jun 2014 23:50:39 +1000 Subject: [PATCH 326/515] Test add_term_data --- tests/test_json_taxonomies.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 3a56240d25..7d39336f96 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -303,6 +303,19 @@ public function test_add_taxonomy_data() { $this->assertArrayNotHasKey( 'taxonomies', $data_within_taxonomy ); } + public function test_add_term_data() { + $post = $this->factory->post->create(); + $post_obj = get_post( $post, ARRAY_A ); + $category = $this->factory->category->create(); + wp_set_post_categories( $post, $category ); + + // This record is not a taxonomy record: taxonomies should be embedded + $data = $this->endpoint->add_term_data( array(), $post_obj, false ); + + $this->assertCount( 1, $data['terms']['category'] ); + $this->assertEquals( $category, $data['terms']['category'][0]['ID'] ); + } + public function test_prepare_taxonomy_term() { $term = get_term( 1, 'category' ); $data = $this->call_protected( 'prepare_taxonomy_term', array( $term ) ); From 3b314edd3f2d7aa9543acf66aa1f3b45bcdf935e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 00:08:34 +1000 Subject: [PATCH 327/515] Only expose email for edit context Hides the email field entirely for the embed context, and sets to false for the view context. --- lib/class-wp-json-users.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 2419433fc8..efbc5d7c24 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -158,7 +158,6 @@ protected function prepare_user( $user, $context = 'view' ) { 'URL' => $user->user_url, 'avatar' => $this->server->get_avatar_url( $user->user_email ), 'description' => $user->description, - 'email' => $user->user_email, ); $user_fields['registered'] = date( 'c', strtotime( $user->user_registered ) ); @@ -166,11 +165,13 @@ protected function prepare_user( $user, $context = 'view' ) { if ( $context === 'view' || $context === 'edit' ) { $user_fields['roles'] = $user->roles; $user_fields['capabilities'] = $user->allcaps; + $user_fields['email'] = false; } if ( $context === 'edit' ) { // The user's specific caps should only be needed if you're editing // the user, as allcaps should handle most uses + $user_fields['email'] = $user->user_email; $user_fields['extra_capabilities'] = $user->caps; } From 11062d4a74f2b14bab7f658d76ac0043c2e311d2 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 00:10:51 +1000 Subject: [PATCH 328/515] Update tests to match email privacy --- tests/test_json_users.php | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/test_json_users.php b/tests/test_json_users.php index df5fdcb5e6..5b0ea1b84c 100644 --- a/tests/test_json_users.php +++ b/tests/test_json_users.php @@ -52,16 +52,36 @@ public function test_get_user() { $this->check_get_user_response( $response, $this->user_obj ); } - protected function check_get_user_response( $response, $user_obj ) { + public function test_get_user_with_edit_context() { + $response = $this->endpoint->get_user( $this->user, 'edit' ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + + if ( ! $response instanceof WP_JSON_ResponseInterface ) { + $response = new WP_JSON_Response( $response ); + } + + // Check that we succeeded + $this->assertEquals( 200, $response->get_status() ); + + $this->check_get_user_response( $response, $this->user_obj, 'edit' ); + } + + protected function check_get_user_response( $response, $user_obj, $context = 'view' ) { $response_data = $response->get_data(); // Check basic data $this->assertEquals( $user_obj->ID, $response_data['ID'] ); $this->assertEquals( $user_obj->user_login, $response_data['username'] ); - $this->assertEquals( $user_obj->user_email, $response_data['email'] ); + if ( $context === 'view' ) { + $this->assertEquals( false, $response_data['email'] ); - // Check that we didn't get extra data - $this->assertArrayNotHasKey( 'extra_capabilities', $response_data ); + // Check that we didn't get extra data + $this->assertArrayNotHasKey( 'extra_capabilities', $response_data ); + } + else { + $this->assertEquals( $user_obj->user_email, $response_data['email'] ); + $this->assertEquals( $user_obj->caps, $response_data['extra_capabilities'] ); + } } public function test_create_user() { @@ -85,7 +105,7 @@ public function test_create_user() { $this->assertEquals( $data['username'], $response_data['username'] ); $this->assertEquals( $data['username'], $new_user->user_login ); - $this->assertEquals( $data['email'], $response_data['email'] ); + $this->assertEquals( false, $response_data['email'] ); $this->assertEquals( $data['email'], $new_user->user_email ); $this->assertTrue( wp_check_password( $data['password'], $new_user->user_pass ), 'Password check failed' ); From 0e2d1a0b022edd034694774b3527363ce20364bc Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 00:59:21 +1000 Subject: [PATCH 329/515] Move helper functions to global namespace --- lib/class-wp-json-server.php | 69 +++++-------------------- plugin.php | 99 ++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 56 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 3748b7f71a..bd186b9586 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -599,49 +599,29 @@ public function prepare_response( $data ) { /** * Parse an RFC3339 timestamp into a DateTime * + * @deprecated * @param string $date RFC3339 timestamp * @param boolean $force_utc Force UTC timezone instead of using the timestamp's TZ? * @return DateTime */ public function parse_date( $date, $force_utc = false ) { - // Default timezone to the server's current one - $timezone = self::get_timezone(); - - if ( $force_utc ) { - $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date ); - $timezone = new DateTimeZone( 'UTC' ); - } - - // Strip millisecond precision (a full stop followed by one or more digits) - if ( strpos( $date, '.' ) !== false ) { - $date = preg_replace( '/\.\d+/', '', $date ); - } - $datetime = WP_JSON_DateTime::createFromFormat( DateTime::RFC3339, $date, $timezone ); - - return $datetime; + _deprecated_function( __CLASS__ . '::' . __METHOD__, 'WPAPI-1.1', 'json_parse_date' ); + + return json_parse_date( $date, $force_utc ); } /** * Get a local date with its GMT equivalent, in MySQL datetime format * + * @deprecated * @param string $date RFC3339 timestamp * @param boolean $force_utc Should we force UTC timestamp? * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s), null on failure */ public function get_date_with_gmt( $date, $force_utc = false ) { - $datetime = $this->parse_date( $date, $force_utc ); - - if ( empty( $datetime ) ) { - return null; - } - - $datetime->setTimezone( self::get_timezone() ); - $local = $datetime->format( 'Y-m-d H:i:s' ); - - $datetime->setTimezone( new DateTimeZone( 'UTC' ) ); - $utc = $datetime->format('Y-m-d H:i:s'); + _deprecated_function( __CLASS__ . '::' . __METHOD__, 'WPAPI-1.1', 'json_get_date_with_gmt' ); - return array( $local, $utc ); + return json_get_date_with_gmt( $date, $force_utc ); } /** @@ -650,49 +630,26 @@ public function get_date_with_gmt( $date, $force_utc = false ) { * {@see get_avatar()} doesn't return just the URL, so we have to * extract it here. * + * @deprecated * @param string $email Email address * @return string url for the user's avatar */ public function get_avatar_url( $email ) { - $avatar_html = get_avatar( $email ); - // strip the avatar url from the get_avatar img tag. - preg_match('/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches); + _deprecated_function( __CLASS__ . '::' . __METHOD__, 'WPAPI-1.1', 'json_get_avatar_url' ); - if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) { - return esc_url_raw( $matches[1] ); - } - - return ''; + return json_get_avatar_url( $email ); } /** * Get the timezone object for the site * + * @deprecated * @return DateTimeZone */ public function get_timezone() { - static $zone = null; - - if ( $zone !== null ) { - return $zone; - } - - $tzstring = get_option( 'timezone_string' ); - - if ( ! $tzstring ) { - // Create a UTC+- zone if no timezone string exists - $current_offset = get_option( 'gmt_offset' ); - if ( 0 == $current_offset ) { - $tzstring = 'UTC'; - } elseif ( $current_offset < 0 ) { - $tzstring = 'Etc/GMT' . $current_offset; - } else { - $tzstring = 'Etc/GMT+' . $current_offset; - } - } - $zone = new DateTimeZone( $tzstring ); + _deprecated_function( __CLASS__ . '::' . __METHOD__, 'WPAPI-1.1', 'json_get_timezone' ); - return $zone; + return json_get_timezone(); } /** diff --git a/plugin.php b/plugin.php index 0dae4a5c2a..a6e579b4f3 100644 --- a/plugin.php +++ b/plugin.php @@ -427,3 +427,102 @@ function json_ensure_response( $response ) { return new WP_JSON_Response( $response ); } + +/** + * Parse an RFC3339 timestamp into a DateTime + * + * @param string $date RFC3339 timestamp + * @param boolean $force_utc Force UTC timezone instead of using the timestamp's TZ? + * @return DateTime + */ +function json_parse_date( $date, $force_utc = false ) { + // Default timezone to the server's current one + $timezone = json_get_timezone(); + + if ( $force_utc ) { + $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date ); + $timezone = new DateTimeZone( 'UTC' ); + } + + // Strip millisecond precision (a full stop followed by one or more digits) + if ( strpos( $date, '.' ) !== false ) { + $date = preg_replace( '/\.\d+/', '', $date ); + } + $datetime = WP_JSON_DateTime::createFromFormat( DateTime::RFC3339, $date, $timezone ); + + return $datetime; +} + +/** + * Get a local date with its GMT equivalent, in MySQL datetime format + * + * @param string $date RFC3339 timestamp + * @param boolean $force_utc Should we force UTC timestamp? + * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s), null on failure + */ +function json_get_date_with_gmt( $date, $force_utc = false ) { + $datetime = json_parse_date( $date, $force_utc ); + + if ( empty( $datetime ) ) { + return null; + } + + $datetime->setTimezone( json_get_timezone() ); + $local = $datetime->format( 'Y-m-d H:i:s' ); + + $datetime->setTimezone( new DateTimeZone( 'UTC' ) ); + $utc = $datetime->format('Y-m-d H:i:s'); + + return array( $local, $utc ); +} + +/** + * Retrieve the avatar url for a user who provided a user ID or email address. + * + * {@see get_avatar()} doesn't return just the URL, so we have to + * extract it here. + * + * @param string $email Email address + * @return string url for the user's avatar +*/ +function json_get_avatar_url( $email ) { + $avatar_html = get_avatar( $email ); + // strip the avatar url from the get_avatar img tag. + preg_match('/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches); + + if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) { + return esc_url_raw( $matches[1] ); + } + + return ''; +} + +/** + * Get the timezone object for the site + * + * @return DateTimeZone + */ +function json_get_timezone() { + static $zone = null; + + if ( $zone !== null ) { + return $zone; + } + + $tzstring = get_option( 'timezone_string' ); + + if ( ! $tzstring ) { + // Create a UTC+- zone if no timezone string exists + $current_offset = get_option( 'gmt_offset' ); + if ( 0 == $current_offset ) { + $tzstring = 'UTC'; + } elseif ( $current_offset < 0 ) { + $tzstring = 'Etc/GMT' . $current_offset; + } else { + $tzstring = 'Etc/GMT+' . $current_offset; + } + } + $zone = new DateTimeZone( $tzstring ); + + return $zone; +} From 7a318d477d55008f15d514975142bf6673775f4b Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 00:59:37 +1000 Subject: [PATCH 330/515] Use global functions --- lib/class-wp-json-posts.php | 10 +++++----- lib/class-wp-json-users.php | 2 +- tests/test_json_posts.php | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 3878a01154..6b9bdf1419 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -702,7 +702,7 @@ protected function prepare_post( $post, $context = 'view' ) { ); // Dates - $timezone = $this->server->get_timezone(); + $timezone = json_get_timezone(); if ( $post['post_date_gmt'] === '0000-00-00 00:00:00' ) { @@ -1316,13 +1316,13 @@ protected function insert_post( $data ) { // Post date if ( ! empty( $data['date'] ) ) { - $date_data = $this->server->get_date_with_gmt( $data['date'] ); + $date_data = json_get_date_with_gmt( $data['date'] ); if ( ! empty( $date_data ) ) { list( $post['post_date'], $post['post_date_gmt'] ) = $date_data; } } elseif ( ! empty( $data['date_gmt'] ) ) { - $date_data = $this->server->get_date_with_gmt( $data['date_gmt'], true ); + $date_data = json_get_date_with_gmt( $data['date_gmt'], true ); if ( ! empty( $date_data ) ) { list( $post['post_date'], $post['post_date_gmt'] ) = $date_data; @@ -1533,12 +1533,12 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen 'ID' => 0, 'name' => $comment->comment_author, 'URL' => $comment->comment_author_url, - 'avatar' => $this->server->get_avatar_url( $comment->comment_author_email ), + 'avatar' => json_get_avatar_url( $comment->comment_author_email ), ); } // Date - $timezone = $this->server->get_timezone(); + $timezone = json_get_timezone(); $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $comment->comment_date, $timezone ); $fields['date'] = $date->format( 'c' ); diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 2419433fc8..ae5f633501 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -156,7 +156,7 @@ protected function prepare_user( $user, $context = 'view' ) { 'nickname' => $user->nickname, 'slug' => $user->user_nicename, 'URL' => $user->user_url, - 'avatar' => $this->server->get_avatar_url( $user->user_email ), + 'avatar' => json_get_avatar_url( $user->user_email ), 'description' => $user->description, 'email' => $user->user_email, ); diff --git a/tests/test_json_posts.php b/tests/test_json_posts.php index 03540f65db..fcbaffefe9 100644 --- a/tests/test_json_posts.php +++ b/tests/test_json_posts.php @@ -72,7 +72,7 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi } // Check post dates. - $timezone = $this->fake_server->get_timezone(); + $timezone = json_get_timezone(); if ( $post_obj->post_date_gmt === '0000-00-00 00:00:00' ) { $this->assertNull( $response_data['date'] ); From c2c04d7d900bd1cf802911c745a0cb5bd48b0d87 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 00:59:47 +1000 Subject: [PATCH 331/515] Remove unnecessary test This was just incomplete anyway. --- tests/test_json_server.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/test_json_server.php b/tests/test_json_server.php index 944e95fd6d..6cd3238f92 100644 --- a/tests/test_json_server.php +++ b/tests/test_json_server.php @@ -105,12 +105,4 @@ function test_json_serializable() { $this->markTestIncomplete('Missing test implementation.'); } - /** - * Test if local RFC3339 dates are converted to MySQL datetimes with the - * appropriate GMT timezone. - */ - function test_get_date_with_gmt() { - $this->markTestIncomplete('Missing test implementation.'); - } - } From cb7aa050acccc76b65c957bf3e725dd8c8205a75 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 8 Jun 2014 12:48:41 -0500 Subject: [PATCH 332/515] Corrected data parameter name for title of a post. --- docs/routes/posts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index b04b589720..54118df43b 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -26,7 +26,7 @@ Content-Type: application/json The `data` parameter should be an object containing the following key value pairs: -* `post_title` - Title of the post. (string) __*required*__ +* `title` - Title of the post. (string) __*required*__ * `content_raw` - Full text of the post. (string) __*required*__ * `excerpt_raw` - Text for excerpt of the post. (string) *optional* * `name` - Slug of the post. (string) *optional* @@ -166,7 +166,7 @@ The `data` parameter should be an object containing the following key value pairs: * `ID` - Unique ID of the post. (integer) __*required*__ -* `post_title` - Title of the post. (string) __*required*__ +* `title` - Title of the post. (string) __*required*__ * `content_raw` - Full text of the post. (string) __*required*__ * `excerpt_raw` - Text for excerpt of the post. (string) *optional* * `name` - Slug of the post. (string) *optional* From 1e689da7e4ec4232f134a3d24206cf68e5293201 Mon Sep 17 00:00:00 2001 From: Max Cutler Date: Sun, 8 Jun 2014 14:21:39 -0400 Subject: [PATCH 333/515] Convert empty attachment sizes to empty object intead of array WP-API issue #299 --- lib/class-wp-json-media.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/class-wp-json-media.php b/lib/class-wp-json-media.php index b6180fa97e..bc05ecddab 100644 --- a/lib/class-wp-json-media.php +++ b/lib/class-wp-json-media.php @@ -109,6 +109,8 @@ protected function prepare_post( $post, $context = 'single' ) { // Use the same method image_downsize() does $size_data['url'] = str_replace( $img_url_basename, $size_data['file'], $data['source'] ); } + } else { + $data['attachment_meta']['sizes'] = new stdClass; } // Override entity meta keys with the correct links From fa99b6335b71e38322d270e04b7991a24299dd6d Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 8 Jun 2014 15:15:35 -0500 Subject: [PATCH 334/515] Added post_meta data parameter details for the create and edit post endpoints. --- docs/routes/posts.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index 54118df43b..dcd1e051b8 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -63,6 +63,9 @@ pairs: option 'default_ping_status'. (string) *optional* * `sticky` - Sticky status for the post: `true` or `false`. Default is `false`. (boolean) *optional* +* `post_meta` - Post meta entries of the post. Post meta should be an array + of one or more Meta objects for each post meta entry. See the Create Meta + for a Post endpoint for the key value pairs. (array) *optional* ### Response @@ -203,6 +206,9 @@ pairs: option 'default_ping_status'. (string) *optional* * `sticky` - Sticky status for the post: `true` or `false`. Default is `false`. (boolean) *optional* +* `post_meta` - Post meta entries of the post. Post meta should be an array + of one or more Meta objects for each post meta entry. See the Edit Meta + for a Post endpoint for the key value pairs. (array) *optional* ### Response From dcf03464ba1e76bc645c192973fd14bde0a155b4 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 8 Jun 2014 15:17:04 -0500 Subject: [PATCH 335/515] Corrected grammar in password data parameter description for create and edit post endpoints. --- docs/routes/posts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/routes/posts.md b/docs/routes/posts.md index dcd1e051b8..6467998731 100644 --- a/docs/routes/posts.md +++ b/docs/routes/posts.md @@ -47,7 +47,7 @@ pairs: * `author` - Author of the post. Author can be provided as a string of the author's ID or as the User object of the author. Default is current user. (object \| string) *optional* -* `password` - Password for protecting the posts. Default is empty string. +* `password` - Password for protecting the post. Default is empty string. (string) *optional* * `post_parent` - Post ID of the post parent. Default is 0. (integer) *optional* @@ -190,7 +190,7 @@ pairs: * `author` - Author of the post. Author can be provided as a string of the author's ID or as the User object of the author. Default is current user. (object \| string) *optional* -* `password` - Password for protecting the posts. Default is empty string. +* `password` - Password for protecting the post. Default is empty string. (string) *optional* * `post_parent` - Post ID of the post parent. Default is 0. (integer) *optional* From 55ce00d626ab935c2ad7a4d9038a981bb991d1dc Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 8 Jun 2014 16:02:52 -0500 Subject: [PATCH 336/515] Attempting to address PHP 5.2 unit test failures with call_protected test method. --- tests/test_json_taxonomies.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 7d39336f96..08f3c0c0ee 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -146,12 +146,9 @@ public function test_get_taxonomy_object_empty() { } protected function call_protected( $method, $args ) { - $endpoint_class = new ReflectionClass( 'WP_JSON_Taxonomies' ); - $method = $endpoint_class->getMethod( $method ); - $method->setAccessible( true ); - $endpoint = $endpoint_class->newInstance(); - - return $method->invokeArgs( $endpoint, $args ); + $reflection = new ReflectionMethod( 'WP_JSON_Taxonomies', $method ); + $reflection->setAccessible( true ); + return $reflection->invokeArgs( new WP_JSON_Taxonomies(), $args ); } /** From 4c37dbe715e90efb6d527e6e23ca6bbdf4efd93b Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 12:57:39 +1000 Subject: [PATCH 337/515] Use subclass for taxonomy protected calls --- tests/taxonomies_caller.php | 7 +++++++ tests/test_json_taxonomies.php | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 tests/taxonomies_caller.php diff --git a/tests/taxonomies_caller.php b/tests/taxonomies_caller.php new file mode 100644 index 0000000000..3a68391934 --- /dev/null +++ b/tests/taxonomies_caller.php @@ -0,0 +1,7 @@ +setAccessible( true ); - return $reflection->invokeArgs( new WP_JSON_Taxonomies(), $args ); + require_once dirname( __FILE__ ) . '/taxonomies_caller.php'; + + $class = new WP_Test_JSON_Taxonomies_Caller(); + return $class->testProtectedCall( $method, $args ); } /** From 637271d8ae7d39fd6a3cba52dce84352a5165667 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 15:51:59 +1000 Subject: [PATCH 338/515] Always add self/collection links --- lib/class-wp-json-posts.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 96bb0db2f5..b0417003a3 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -585,16 +585,13 @@ public function get_post_type( $type, $_in_collection = false, $_in_taxonomy = f 'searchable' => ! $type->exclude_from_search, 'hierarchical' => $type->hierarchical, 'meta' => array( - 'links' => array() + 'links' => array( + 'self' => json_url( '/posts/types/' . $type->name ), + 'collection' => json_url( '/posts/types' ), + ), ), ); - if ( $_in_collection || $_in_taxonomy ) { - $data['meta']['links']['self'] = json_url( '/posts/types/' . $type->name ); - } else { - $data['meta']['links']['collection'] = json_url( '/posts/types' ); - } - // Add taxonomy link $relation = 'http://wp-api.org/1.1/collections/taxonomy/'; $url = json_url( '/taxonomies' ); From 8419998cb709e11c2b3d6e5393974e0d1572fe52 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 16:20:24 +1000 Subject: [PATCH 339/515] Remove _in_taxonomy in favour of context --- lib/class-wp-json-posts.php | 14 +++++++++----- lib/class-wp-json-taxonomies.php | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index b0417003a3..fcc770c186 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -563,11 +563,10 @@ public function get_post_types() { * Get a post type * * @param string|object $type Type name, or type object (internal use) - * @param boolean $_in_collection Is this in a collection? (internal use) - * @param boolean $_in_taxonomy Is this request being added to a taxonomy record? (internal use) + * @param boolean $context What context are we in? * @return array Post type data */ - public function get_post_type( $type, $_in_collection = false, $_in_taxonomy = false ) { + public function get_post_type( $type, $context = 'view' ) { if ( ! is_object( $type ) ) { $type = get_post_type_object( $type ); } @@ -576,6 +575,11 @@ public function get_post_type( $type, $_in_collection = false, $_in_taxonomy = f return new WP_Error( 'json_cannot_read_type', __( 'Cannot view post type' ), array( 'status' => 403 ) ); } + if ( $context === true ) { + $context = 'embed'; + _deprecated_argument( __CLASS__ . '::' . __FUNCTION__, 'WPAPI-1.1', '$context should be set to "embed" rather than true' ); + } + $data = array( 'name' => $type->label, 'slug' => $type->name, @@ -606,7 +610,7 @@ public function get_post_type( $type, $_in_collection = false, $_in_taxonomy = f } } - return apply_filters( 'json_post_type_data', $data, $type, $_in_taxonomy ); + return apply_filters( 'json_post_type_data', $data, $type, $context ); } /** @@ -1225,7 +1229,7 @@ public function add_post_type_data( $data, $taxonomy, $context = 'view' ) { if ( $context !== 'embed' ) { $data['types'] = array(); foreach( $taxonomy->object_type as $type ) { - $data['types'][ $type ] = $this->get_post_type( $type, false, true ); + $data['types'][ $type ] = $this->get_post_type( $type, 'embed' ); } } diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 44e0a11ea5..2b4a73bd85 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -177,8 +177,8 @@ protected function prepare_taxonomy_object( $taxonomy, $context = 'view' ) { * @param boolean $_in_taxonomy The record being filtered is a taxonomy object (internal use) * @return array Filtered data */ - public function add_taxonomy_data( $data, $type, $_in_taxonomy = false ) { - if ( ! $_in_taxonomy ) { + public function add_taxonomy_data( $data, $type, $context = 'view' ) { + if ( $context !== 'embed' ) { $data['taxonomies'] = $this->get_taxonomies( $type->name, 'embed' ); } From df8365c0e452b3ec60f357dcff13038dc6a7805b Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 16:43:51 +1000 Subject: [PATCH 340/515] Update tests for context This isn't a BC break, since we only introduced it with this. --- tests/test_json_taxonomies.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_json_taxonomies.php b/tests/test_json_taxonomies.php index 82737b67d9..2fcfcb1e75 100644 --- a/tests/test_json_taxonomies.php +++ b/tests/test_json_taxonomies.php @@ -293,11 +293,11 @@ public function test_add_taxonomy_data() { $type->name = 'post'; // This record is not a taxonomy record: taxonomies should be embedded - $data = $this->endpoint->add_taxonomy_data( array(), $type, false ); + $data = $this->endpoint->add_taxonomy_data( array(), $type, 'view' ); $this->assertArrayHasKey( 'taxonomies', $data ); // This record is a taxonomy record: taxonomies should NOT be embedded - $data_within_taxonomy = $this->endpoint->add_taxonomy_data( array(), $type, true ); + $data_within_taxonomy = $this->endpoint->add_taxonomy_data( array(), $type, 'embed' ); $this->assertArrayNotHasKey( 'taxonomies', $data_within_taxonomy ); } From 6271eb344f3558d6b732bf58ba1851fa903cd931 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 17:17:18 +1000 Subject: [PATCH 341/515] Update description in README --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 40cec1282b..3b80b49a2e 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,48 @@ This is a project to create a JSON-based REST API for WordPress. This project is run by Ryan McCue and is part of the WordPress 2013 GSoC projects. +Access your WordPress site's data through an easy-to-use HTTP REST API. + [![Build Status](https://travis-ci.org/WP-API/WP-API.png?branch=master)](https://travis-ci.org/WP-API/WP-API) +## About + +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. + +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. + +Want to get your site's posts? Simply send a `GET` request to `/wp-json/posts`. +Update user with ID 4? Send a `POST` request to `/wp-json/users/4`. Get all +posts with the search term "awesome"? `GET /wp-json/posts?filter[s]=awesome`. +It's that easy. + +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. + +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. + +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! + +We're currently aiming for integration into WordPress 4.1 as a permanent part of +core. + +All tickets for the project are being tracked on [GitHub][]. You can also take a +look at the [recent updates][] for the project. + +[docs]: https://github.com/WP-API/WP-API/tree/master/docs +[GitHub]: https://github.com/WP-API/WP-API +[recent updates]: http://make.wordpress.org/core/tag/json-api/ + ## Documentation @@ -14,19 +54,9 @@ Read the [plugin's documentation][docs]. ## Installation -### As a Plugin Drop this directory in and activate it. You need to be using pretty permalinks to use the plugin, as it uses custom rewrite rules to power the API. -### As Part of Core -**Note: These instructions will likely be broken while in development. Please -use the plugin method instead.** - -Drop `wp-json.php` into your WordPress directory, and drop -`class-wp-json-server.php` into your `wp-includes/` directory. You'll need -working `PATH_INFO` on your server, but you don't need pretty permalinks -enabled. - ## Quick Setup From b534d2757b9b8caa5373e6570340afdb68cb6503 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 17:27:00 +1000 Subject: [PATCH 342/515] Update testing instructions --- README.md | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3b80b49a2e..07b31fae58 100644 --- a/README.md +++ b/README.md @@ -105,25 +105,17 @@ still "Default," to http://vagrant.local/?json_route=/) For testing, you'll need a little bit more: -1. SSH into your Vagrant box, and install PHPUnit: +1. Clone the [Tester extension](https://github.com/Chassis/Tester) for Chassis: ```bash - vagrant ssh - sudo apt-get install php-pear - sudo pear config-set auto_discover 1 - sudo pear install pear.phpunit.de/PHPUnit - ``` - -2. Clone WordPress development (including tests): - - ```bash - git clone git://develop.git.wordpress.org/ /tmp/wordpress - export WP_DEVELOP_DIR=/tmp/wordpress + # From your base directory, api-tester if following the steps from before + git clone --recursive https://github.com/Chassis/Tester.git extensions/tester ``` 3. Run the testing suite: ```bash + vagrant ssh cd /vagrant/content/plugins/json-rest-api phpunit ``` From 8f3626a6163ba929c243dcd76d21182744844cd9 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 17:29:14 +1000 Subject: [PATCH 343/515] Remove redundant information --- README.md | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 07b31fae58..c96731e83b 100644 --- a/README.md +++ b/README.md @@ -37,20 +37,6 @@ extra data for plugin and theme developers! We're currently aiming for integration into WordPress 4.1 as a permanent part of core. -All tickets for the project are being tracked on [GitHub][]. You can also take a -look at the [recent updates][] for the project. - -[docs]: https://github.com/WP-API/WP-API/tree/master/docs -[GitHub]: https://github.com/WP-API/WP-API -[recent updates]: http://make.wordpress.org/core/tag/json-api/ - - -## Documentation - -Read the [plugin's documentation][docs]. - -[docs]: https://github.com/WP-API/WP-API/tree/master/docs - ## Installation @@ -123,9 +109,13 @@ For testing, you'll need a little bit more: ## Issue Tracking -All tickets for the project are being tracked on [GitHub][]. Previous issues can -be found on the [GSOC Trac][] issue tracker, however new issues should not be -filed there. +All tickets for the project are being tracked on [GitHub][]. You can also take a +look at the [recent updates][] for the project. + +Previous issues can be found on the [GSOC Trac][] issue tracker, however new +issues should not be filed there. +[docs]: https://github.com/WP-API/WP-API/tree/master/docs [GitHub]: https://github.com/WP-API/WP-API [GSOC Trac]: https://gsoc.trac.wordpress.org/query?component=JSON+REST+API +[recent updates]: http://make.wordpress.org/core/tag/json-api/ From 0078f4903ec8db2b5b26e56d39cfcf1ec8d94ced Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 17:30:52 +1000 Subject: [PATCH 344/515] Remove old short description --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index c96731e83b..27bb63a2a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ # REST API -This is a project to create a JSON-based REST API for WordPress. This project is -run by Ryan McCue and is part of the WordPress 2013 GSoC projects. Access your WordPress site's data through an easy-to-use HTTP REST API. From e768a7254d616d0f45c104654a4778ff4bc4914d Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 17:31:09 +1000 Subject: [PATCH 345/515] Remove hostsupdater requirement Chassis now uses Bonjour instead. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 27bb63a2a6..2af8f01b3c 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,6 @@ testing environment in a few easy steps: ```bash git clone --recursive git@github.com:sennza/Chassis.git api-tester - vagrant plugin install vagrant-hostsupdater ``` 3. Grab a copy of WP API: From a52f46b15f1ae94d01e6744c4517825d4366b102 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 9 Jun 2014 18:31:46 +1000 Subject: [PATCH 346/515] Fix data-binary cURL option --- docs/guides/getting-started.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index d7e4d321a3..966cb48e72 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -419,7 +419,7 @@ modification date: Save the data as "updated-post.json", then we can send this to the server with the correct headers and authentication. The API uses HTTP Basic authentication: - curl --data-binary="@updated-post.json" \ + curl --data-binary "@updated-post.json" \ -H "Content-Type: application/javascript" \ --user admin:password \ http://example.com/wp-json/posts/1 @@ -434,7 +434,7 @@ more details on this. Similarly to editing posts, you can create posts. We can use the same data from before, but this time, we POST it to the main posts route. - curl --data-binary="@updated-post.json" \ + curl --data-binary "@updated-post.json" \ -H "Content-Type: application/javascript" \ --user admin:password \ http://example.com/wp-json/posts From 5511797d64db8451444ccf4cda5422bd4da59307 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 9 Jun 2014 09:46:56 -0500 Subject: [PATCH 347/515] Added authentication details to edit and create posts examples in Getting Started doc. --- docs/guides/getting-started.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 966cb48e72..274cd6fa2c 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -406,8 +406,8 @@ Editing and Creating Posts -------------------------- Just as we can use a GET request to get a post, we can use PUT to edit a post. The easiest way to do this is to send a JSON body back to the server with just -the fields you want to change. For example, to edit the title and the -modification date: +the fields you want to change. [Authentication][auth] is **required** to edit +posts. For example, to edit the title and the modification date: ```json { @@ -417,7 +417,9 @@ modification date: ``` Save the data as "updated-post.json", then we can send this to the server with -the correct headers and authentication. The API uses HTTP Basic authentication: +the correct headers and authentication. We will provide our username and +password with HTTP Basic authentication, which requires the [Basic Auth plugin][basic-auth-plugin] +be installed: curl --data-binary "@updated-post.json" \ -H "Content-Type: application/javascript" \ @@ -431,8 +433,10 @@ Note that there are some fields we can't update; ID is an obvious example, but others like timezone fields cannot be updated either. Check the [schema][] for more details on this. -Similarly to editing posts, you can create posts. We can use the same data from -before, but this time, we POST it to the main posts route. +Similarly to editing posts, you can create posts. [Authentication][auth] is +**required** to create posts. We can use the same data from before, but this time, we POST it to the main posts route. Again, we are providing our username +and password using HTTP Basic authentication which requires the [Basic Auth plugin][basic-auth-plugin] +be installed: curl --data-binary "@updated-post.json" \ -H "Content-Type: application/javascript" \ @@ -472,6 +476,9 @@ get exploring! * [Working With Posts][]: Learn more about Posts and their data * [Schema][schema]: View technical information on all the available data +* [Authentication][auth] : Explore authentication options [Working with Posts]: working-with-posts.md [schema]: ../schema.md +[auth]: ../authentication.md +[basic-auth-plugin]: https://github.com/WP-API/Basic-Auth From ed2cacd0aa714b485be5146b164cdbfad1bde596 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 10 Jun 2014 10:17:43 +1000 Subject: [PATCH 348/515] Ensure lines are wrapped --- docs/guides/getting-started.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 274cd6fa2c..1d7b1b5ed8 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -434,9 +434,10 @@ others like timezone fields cannot be updated either. Check the [schema][] for more details on this. Similarly to editing posts, you can create posts. [Authentication][auth] is -**required** to create posts. We can use the same data from before, but this time, we POST it to the main posts route. Again, we are providing our username -and password using HTTP Basic authentication which requires the [Basic Auth plugin][basic-auth-plugin] -be installed: +**required** to create posts. We can use the same data from before, but this +time, we POST it to the main posts route. Again, we are providing our username +and password using HTTP Basic authentication which requires the +[Basic Auth plugin][basic-auth-plugin] be installed: curl --data-binary "@updated-post.json" \ -H "Content-Type: application/javascript" \ @@ -476,7 +477,7 @@ get exploring! * [Working With Posts][]: Learn more about Posts and their data * [Schema][schema]: View technical information on all the available data -* [Authentication][auth] : Explore authentication options +* [Authentication][auth]: Explore authentication options [Working with Posts]: working-with-posts.md [schema]: ../schema.md From 39ebf84a957bec00d020ef87c71eef8c329ff727 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 15 Jun 2014 13:58:29 +1000 Subject: [PATCH 349/515] If the post has a password, hide it for non-editors --- lib/class-wp-json-posts.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 55aed8c27c..5d1de83677 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -205,7 +205,12 @@ public function get_posts( $filter = array(), $context = 'view', $type = 'post', } $response->link_header( 'item', json_url( '/posts/' . $post['ID'] ), array( 'title' => $post['post_title'] ) ); - $struct[] = $this->prepare_post( $post, $context ); + $post_data = $this->prepare_post( $post, $context ); + if ( is_wp_error( $post_data ) ) { + continue; + } + + $struct[] = $post_data; } $response->set_data( $struct ); @@ -676,6 +681,12 @@ protected function prepare_post( $post, $context = 'view' ) { $previous_post = $GLOBALS['post']; } $post_obj = get_post( $post['ID'] ); + + // Don't allow unauthenticated users to read password-protected posts + if ( ! empty( $post['post_password'] ) && ! $this->check_edit_permission( $post ) ) { + return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 403 ) ); + } + $GLOBALS['post'] = $post_obj; setup_postdata( $post_obj ); From c10825f1c79ef1b2302f5d57e6b75ca479494767 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 15 Jun 2014 14:03:06 +1000 Subject: [PATCH 350/515] Fake password input for editors This is absolutely ridiculous, but there's no way around it, as core internally hardcodes the password check **inside get_the_content()** --- lib/class-wp-json-posts.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 5d1de83677..4d7423966a 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -683,8 +683,17 @@ protected function prepare_post( $post, $context = 'view' ) { $post_obj = get_post( $post['ID'] ); // Don't allow unauthenticated users to read password-protected posts - if ( ! empty( $post['post_password'] ) && ! $this->check_edit_permission( $post ) ) { - return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 403 ) ); + if ( ! empty( $post['post_password'] ) ) { + if ( ! $this->check_edit_permission( $post ) ) { + return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 403 ) ); + } + + // Fake the correct cookie to fool post_password_required(). + // Without this, get_the_content() will give a password form. + require_once ABSPATH . 'wp-includes/class-phpass.php'; + $hasher = new PasswordHash( 8, true ); + $value = $hasher->HashPassword( $post['post_password'] ); + $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] = wp_slash( $value ); } $GLOBALS['post'] = $post_obj; From 3e0002a7cb6c9dfe5ae9466a00a8c84ccbc41e52 Mon Sep 17 00:00:00 2001 From: Travis Hensgen Date: Tue, 17 Jun 2014 13:54:19 +1000 Subject: [PATCH 351/515] added to json_prepare_term filter call, for concistency with other prepare filter applications --- lib/class-wp-json-taxonomies.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 2b4a73bd85..c256c6cd77 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -338,6 +338,6 @@ protected function prepare_taxonomy_term( $term, $context = 'view' ) { $data['parent'] = null; } - return apply_filters( 'json_prepare_term', $data, $term ); + return apply_filters( 'json_prepare_term', $data, $term, $context ); } } From 4ca305cb73e9043cdb786aa8b10a0f9725593785 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 17 Jun 2014 20:07:13 +1000 Subject: [PATCH 352/515] Add changelog for 1.1 --- CHANGELOG.md | 276 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea861394e8..d2699b0ed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,281 @@ # Changelog +## 1.1 + +- Add new routes for taxonomies and terms. + + Taxonomies and terms have now been moved from the `/posts/types/` + namespace to global routes: `/taxonomies`, `/taxonomies/`, + `/taxonomies//terms` and `/taxonomies//terms/` + + Test coverage for taxonomy endpoints has also been increased to 100%. + + **Deprecation warning**: The `/posts/types//taxonomies` endpoint (and + sub-endpoints with the same prefix) have been deprecated in favour of the new + endpoints. These deprecated endpoints will now return a + `X-WP-DeprecatedFunction` header indicating that the endpoint should not be + used for new development, but will continue to work in the future. + + (props @kadamwhite, @rachelbaker, @rmccue, [#198][gh-198], [#211][gh-211]) + +- Allow customizing the API resources prefix + + The API base (typically `wp-json/`) can now be customized to a different + prefix using the `json_url_prefix` filter. Note that rewrites will need to be + flushed manually after changing this. + + (props @ericandrewlewis, @rmccue, [#104][gh-104], [#244][gh-244], [#278][gh-278]) + +- Give `null` as date for draft posts. + + Draft posts would previously return "0000-00-00 00:00:00" or + "1970-01-01T00:00:00", as draft posts are not assigned a publish date. The API + now returns `null` where a date is not available. + + **Compatibility warning**: Clients should be prepared to accept `null` as a + value for date/time fields, and treat it as if no value is set. + + (props @rmccue, [#229][gh-229], [#230][gh-230]) + +- Fix errors with excerpt. + + Posts without excerpts could previously return nonsense strings, excerpts from + other posts, or cause internal PHP errors. Posts without excerpts will now + always return an excerpt, typically automatically generated from the post + content. + + The `excerpt_raw` field was added to the edit context on posts. This field + contains the raw excerpt data saved for the post, including empty + string values. + + (props @rmccue, [#222][gh-226], [#226][gh-226]) + +- Only expose email for edit context. + + User email addresses are now only exposed for `context=edit`, which requires + the `edit_users` permission (not required for the current user). + + The email address field will now return `false` instead of a string if the + field is not exposed. + + (props @pkevan, @rmccue, [#290][gh-290], [#296][gh-296]) + +- Correct password-protected post handling. + + Password-protected posts could previously be exposed to all users, however + could also have broken behaviour with excerpts. Password-protected posts are + now hidden to unauthenticated users, while content and excerpts are shown + correctly for the `edit` context. + + (Note that hiding password-protected posts is intended to be a temporary + measure, and will likely change in the future.) + + (props @rmccue, [#286][gh-286], [#313][gh-313]) + +- Add documentation on authentication methods. + + Full documentation on [authentication](https://github.com/WP-API/WP-API/blob/master/docs/authentication.md) + is now available. This documentation explains the difference between the + various available authentication methods, and notes which should be used. + + (props @rmccue, [#242][gh-242]) + +- Include new client JS from github.io + + The WP-API Javascript library is now loaded dynamically from + `wp-api.github.io` to ensure it is always up-to-date. + + (props @tlovett1, [#179][gh-179], [#240][gh-240]) + +- Don't allow setting the modification date on post creation/update. + + As it turns out, WP core doesn't allow us to set this, so this was previously + a no-op anyway. Discovered during test coverage phase. + + (props @rachelbaker, @rmccue, [#285][gh-285], [#288][gh-288]) + +- Check post parent correctly on insertion. + + Posts could previously be added with an invalid parent ID. These IDs are now + checked to ensure the post exists. + + (props @rmccue, [#228][gh-228], [#231][gh-231]) + +- Make sure the type is actually evaluated for `json_prepare_${type}` filter. + + This value was previously not interpolated correctly, due to the use of the + single-quoted string type. + + (props @danielbachhuber, [#266][gh-266]) + +- Return `WP_Error` instead of array of empty objects for a revisions + permissions error. + + Previously, when trying to access post revisions without correct permissions, + a JSON list of internal error objects would be returned. This has been + corrected to return a standard API error instead. + + (props @rachelbaker, @tlovett1, [#251][gh-251], [#276][gh-276]) + +- Flip user parameters check for insert/update. + + Previously, you could add a user without specifying username/password/email, + but couldn't update a user without those parameters. The logic has been + inverted here instead. + + (props @rmccue, [#221][gh-221], [#289][gh-289]) + +- Add revision endpoints tests + + (props @danielbachhuber, @rachelbaker, @rmccue, [#275][gh-275], [#277][gh-277], [#284][gh-284], [#279][gh-279]) + +- Add post endpoint testing + + Now at >54% coverage for the whole class, and >80% for the main methods. This + figure will continue to rise over the next few releases. + + (props @rachelbaker, @rmccue, [#99][gh-99]) + +- Separate helper functions into global namespace. + + `WP_JSON_Server::get_timezone()`, `WP_JSON_Server::get_date_with_gmt()`, + `WP_JSON_Server::get_avatar_url()` and ``WP_JSON_Server::parse_date()` have + all been moved into the global namespace to decouple them from the server + class. + + **Deprecation warning**: These methods have been deprecated. The new + `json_get_timezone()`, `json_get_date_with_gmt()`, `json_get_avatar_url()` and + `json_parse_date()` methods should now be used instead. + + (props @rmccue, [#185][gh-185], [#298][gh-298]) + +- Re-order Users and Media routes documentation based on CRUD order + + (props @rachelbaker, [#214][gh-214]) + +- Update Post route documentation to provide more detail for data parameter + + (props @rachelbaker, [#212][gh-212]) + +- Correct documentation typo ("inforcement" -> "enforcement"). + + (props @ericandrewlewis, [#236][gh-236]) + +- Coding Standards audit + + (props @DrewAPicture, [#235][gh-235]) + +- Add comparison documentation. + + (props @rachelbaker, @rmccue, [#217][gh-225], [#225][gh-225]) + +- `json_url` filter call should be passed `$scheme` + + (props @ericandrewlewis, [#243][gh-243]) + +- Set `class-jsonserializable.php` file mode to 644. + + (props @jeremyfelt, [#255][gh-255]) + +- Remove unneeded "which" in implementation doc. + + (props @JDGrimes, [#254][gh-254]) + +- Fix a copy/paste error in schema doc. + + (props @JDGrimes, [#253][gh-253]) + +- Correct reference link in example schema. + + (props @danielbachhuber, [#258][gh-258]) + +- Add missing post formats to post schema documentation. + + (props @danielbachhuber, [#260][gh-260]) + +- Ensure we always use "public" on public methods. + + (props @danielbachhuber, [#268][gh-268]) + +- Ensure we don't cause a PHP error if a post does not have revisions. + + (props @rmccue, [#227][gh-227]) + +- Add note to where upload_files cap comes from + + (props @pkevan, [#282][gh-282]) + +- Add handling of `sticky` property when creating or editing posts. + + (props @rachelbaker, [#218][gh-218]) + +- Update post route endpoint docs to include details on `post_meta` handling. + + (props @rachelbaker, [#213][gh-213]) + +- Update main readme file to better describe the project. + + (props @rmccue, [#303][gh-303]) + +- Fix `--data-binary` cURL option in documentation + + (props @Pezzab, @rachelbaker, @rmccue, [#283][gh-283], [#304][gh-304]) + +[View all changes](https://github.com/rmccue/WP-API/compare/1.0...1.1) + +[gh-99]: https://github.com/WP-API/WP-API/issues/99 +[gh-104]: https://github.com/WP-API/WP-API/issues/104 +[gh-179]: https://github.com/WP-API/WP-API/issues/179 +[gh-185]: https://github.com/WP-API/WP-API/issues/185 +[gh-198]: https://github.com/WP-API/WP-API/issues/198 +[gh-211]: https://github.com/WP-API/WP-API/issues/211 +[gh-212]: https://github.com/WP-API/WP-API/issues/212 +[gh-213]: https://github.com/WP-API/WP-API/issues/213 +[gh-214]: https://github.com/WP-API/WP-API/issues/214 +[gh-218]: https://github.com/WP-API/WP-API/issues/218 +[gh-221]: https://github.com/WP-API/WP-API/issues/221 +[gh-225]: https://github.com/WP-API/WP-API/issues/225 +[gh-225]: https://github.com/WP-API/WP-API/issues/225 +[gh-226]: https://github.com/WP-API/WP-API/issues/226 +[gh-226]: https://github.com/WP-API/WP-API/issues/226 +[gh-227]: https://github.com/WP-API/WP-API/issues/227 +[gh-228]: https://github.com/WP-API/WP-API/issues/228 +[gh-229]: https://github.com/WP-API/WP-API/issues/229 +[gh-230]: https://github.com/WP-API/WP-API/issues/230 +[gh-231]: https://github.com/WP-API/WP-API/issues/231 +[gh-235]: https://github.com/WP-API/WP-API/issues/235 +[gh-236]: https://github.com/WP-API/WP-API/issues/236 +[gh-240]: https://github.com/WP-API/WP-API/issues/240 +[gh-242]: https://github.com/WP-API/WP-API/issues/242 +[gh-243]: https://github.com/WP-API/WP-API/issues/243 +[gh-244]: https://github.com/WP-API/WP-API/issues/244 +[gh-251]: https://github.com/WP-API/WP-API/issues/251 +[gh-253]: https://github.com/WP-API/WP-API/issues/253 +[gh-254]: https://github.com/WP-API/WP-API/issues/254 +[gh-255]: https://github.com/WP-API/WP-API/issues/255 +[gh-258]: https://github.com/WP-API/WP-API/issues/258 +[gh-260]: https://github.com/WP-API/WP-API/issues/260 +[gh-266]: https://github.com/WP-API/WP-API/issues/266 +[gh-268]: https://github.com/WP-API/WP-API/issues/268 +[gh-275]: https://github.com/WP-API/WP-API/issues/275 +[gh-276]: https://github.com/WP-API/WP-API/issues/276 +[gh-277]: https://github.com/WP-API/WP-API/issues/277 +[gh-278]: https://github.com/WP-API/WP-API/issues/278 +[gh-279]: https://github.com/WP-API/WP-API/issues/279 +[gh-282]: https://github.com/WP-API/WP-API/issues/282 +[gh-283]: https://github.com/WP-API/WP-API/issues/283 +[gh-284]: https://github.com/WP-API/WP-API/issues/284 +[gh-285]: https://github.com/WP-API/WP-API/issues/285 +[gh-286]: https://github.com/WP-API/WP-API/issues/286 +[gh-288]: https://github.com/WP-API/WP-API/issues/288 +[gh-289]: https://github.com/WP-API/WP-API/issues/289 +[gh-290]: https://github.com/WP-API/WP-API/issues/290 +[gh-296]: https://github.com/WP-API/WP-API/issues/296 +[gh-298]: https://github.com/WP-API/WP-API/issues/298 +[gh-303]: https://github.com/WP-API/WP-API/issues/303 +[gh-304]: https://github.com/WP-API/WP-API/issues/304 +[gh-313]: https://github.com/WP-API/WP-API/issues/313 + ## 1.0 - Add user endpoints. From 9e019bbb76f905615723c1dd01bb45ff08d9c33c Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 17 Jun 2014 20:08:45 +1000 Subject: [PATCH 353/515] Bump version to 1.1 --- plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.php b/plugin.php index 36888f7ed1..41c1875852 100644 --- a/plugin.php +++ b/plugin.php @@ -4,7 +4,7 @@ * Description: JSON-based REST API for WordPress, developed as part of GSoC 2013. * Author: Ryan McCue * Author URI: http://ryanmccue.info/ - * Version: 1.0 + * Version: 1.1 * Plugin URI: https://github.com/rmccue/WP-API */ @@ -13,7 +13,7 @@ * * @var string */ -define( 'JSON_API_VERSION', '1.0' ); +define( 'JSON_API_VERSION', '1.1' ); /** * Include our files for the API From d7e695de0cf9fdac57b2fcada8678219b3e78bb0 Mon Sep 17 00:00:00 2001 From: Sarah Gooding Date: Thu, 19 Jun 2014 00:25:06 -0500 Subject: [PATCH 354/515] Updated link to docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2af8f01b3c..5302b85ca0 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ look at the [recent updates][] for the project. Previous issues can be found on the [GSOC Trac][] issue tracker, however new issues should not be filed there. -[docs]: https://github.com/WP-API/WP-API/tree/master/docs +[docs]: http://wp-api.org/ [GitHub]: https://github.com/WP-API/WP-API [GSOC Trac]: https://gsoc.trac.wordpress.org/query?component=JSON+REST+API [recent updates]: http://make.wordpress.org/core/tag/json-api/ From 1e2914852c64d6029de6ab3002df6a5be98664ea Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Thu, 19 Jun 2014 20:42:49 -0500 Subject: [PATCH 355/515] Added content_raw parameter and renamed date parameter for getting started example. --- docs/guides/getting-started.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index b0aaacefe6..95b7ccda7c 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -395,12 +395,13 @@ Editing and Creating Posts Just as we can use a GET request to get a post, we can use PUT to edit a post. The easiest way to do this is to send a JSON body back to the server with just the fields you want to change. [Authentication][auth] is **required** to edit -posts. For example, to edit the title and the modification date: +posts. For example, to edit the title, content, and the date: ```json { "title": "Hello Updated World!", - "modified": "2013-04-01T14:00:00+10:00" + "content_raw": "

Howdy updated content<\/p>", + "date": "2013-04-01T14:00:00+10:00" } ``` From 19d5a3ddb32845c6ebfaf41c3e0c7eb24f4a66f1 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Thu, 19 Jun 2014 20:45:58 -0500 Subject: [PATCH 356/515] Added period to end of content_raw value in getting started example. --- docs/guides/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 95b7ccda7c..9a1b982de4 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -400,7 +400,7 @@ posts. For example, to edit the title, content, and the date: ```json { "title": "Hello Updated World!", - "content_raw": "

Howdy updated content<\/p>", + "content_raw": "

Howdy updated content.<\/p>", "date": "2013-04-01T14:00:00+10:00" } ``` From 9fa962105fe690a910c109e6f017f91e4a0771f8 Mon Sep 17 00:00:00 2001 From: JDGrimes Date: Sat, 21 Jun 2014 18:05:41 -0400 Subject: [PATCH 357/515] Remove references to removed $fields parameter The `get_post()` and `prepare_post()` methods apparently had a `$fields` parameter at one time. This has been replaced by the `$context` parameter. When the `$fields` parameter was removed, some of the inline docs stayed behind. This removes them. --- lib/class-wp-json-customposttype.php | 2 +- lib/class-wp-json-pages.php | 2 +- lib/class-wp-json-posts.php | 8 ++------ 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/class-wp-json-customposttype.php b/lib/class-wp-json-customposttype.php index f82fb389de..faf0a5573e 100644 --- a/lib/class-wp-json-customposttype.php +++ b/lib/class-wp-json-customposttype.php @@ -189,7 +189,7 @@ public function delete_post( $id, $force = false ) { * Prepare post data * * @param array $post The unprepared post data - * @param array $fields The subset of post type fields to return + * @param string $context * @return array The prepared post data */ protected function prepare_post( $post, $context = 'view' ) { diff --git a/lib/class-wp-json-pages.php b/lib/class-wp-json-pages.php index df73f61eca..a4b43d46cf 100644 --- a/lib/class-wp-json-pages.php +++ b/lib/class-wp-json-pages.php @@ -103,7 +103,7 @@ public function delete_post_by_path( $path, $force = false ) { * Prepare post data * * @param array $post The unprepared post data - * @param array $fields The subset of post type fields to return + * @param string $context * @return array The prepared post data */ protected function prepare_post( $post, $context = 'view' ) { diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 4d7423966a..2c4be56ebc 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -116,11 +116,7 @@ public function get_revisions( $id ) { * Accepted keys are 'post_type', 'post_status', 'number', 'offset', * 'orderby', and 'order'. * - * The optional $fields parameter specifies what fields will be included - * in the response array. - * * @uses wp_get_recent_posts() - * @see WP_JSON_Posts::get_post() for more on $fields * @see get_posts() for more on $filter values * * @param array $filter Parameters to pass through to `WP_Query` @@ -317,7 +313,7 @@ public function new_post( $data ) { * * @uses get_post() * @param int $id Post ID - * @param array $fields Post fields to return (optional) + * @param string $context * @return array Post entity */ public function get_post( $id, $context = 'view' ) { @@ -667,7 +663,7 @@ public function get_post_statuses() { * @return array The prepared post data */ protected function prepare_post( $post, $context = 'view' ) { - // holds the data for this post. built up based on $fields + // Holds the data for this post. $_post = array( 'ID' => (int) $post['ID'] ); $post_type = get_post_type_object( $post['post_type'] ); From 44c99d676acc9ac441627a7618a162825ba4674d Mon Sep 17 00:00:00 2001 From: Sarah Gooding Date: Mon, 23 Jun 2014 03:44:51 -0500 Subject: [PATCH 358/515] Removed guides directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guides directory should not exist in two places now that we’ve merged it into the wp-api.org site. We want to make sure all the contributions go to one place. --- docs/guides/README.md | 15 - docs/guides/extending.md | 355 ---------------------- docs/guides/getting-started.md | 473 ------------------------------ docs/guides/working-with-posts.md | 287 ------------------ 4 files changed, 1130 deletions(-) delete mode 100644 docs/guides/README.md delete mode 100644 docs/guides/extending.md delete mode 100644 docs/guides/getting-started.md delete mode 100644 docs/guides/working-with-posts.md diff --git a/docs/guides/README.md b/docs/guides/README.md deleted file mode 100644 index d3559409dc..0000000000 --- a/docs/guides/README.md +++ /dev/null @@ -1,15 +0,0 @@ -Guides -====== -So, you want to learn about the API? Where better to start than with the guides. -Each guide will walk you through interacting with the relevant part of the API -and mention tips and tricks for using it. - -* [Getting Started][]: Begin working with the API, and learn how to access basic - post data. -* [Working with Posts][]: Explore more advanced usage of post-related data, - including filtering and additional data. -* [Extending the API][]: Create your own API routes and endpoints in a plugin. - -[Getting Started]: getting-started.md -[Working with Posts]: working-with-posts.md -[Extending the API]: extending.md diff --git a/docs/guides/extending.md b/docs/guides/extending.md deleted file mode 100644 index 9348e9765e..0000000000 --- a/docs/guides/extending.md +++ /dev/null @@ -1,355 +0,0 @@ -Extending the API -================= - -If you're a plugin author, you're probably itching to start adding functionality -to the API, including working with custom post types. I'm here to guide you -through the process of extending the API with your data, and to remind you of -some important points. - - -Before We Start ---------------- -This guide assumes that you have a fairly decent knowledge of the API already. -Before starting, make sure you've read the [Getting Started][] guide, and if you -want to work with custom post types, also read the [Working With Posts][] guide. - -You should also have a pretty good knowledge of working with actions and filters -in WordPress, as well as how plugins work in general. - -[Getting Started]: getting-started.md -[Working with Posts]: working-with-posts.md - - -A Philosophy Lesson -------------------- -Right off the bat, let me just say that the REST API is not designed for every -use under the sun. Following the mantra of [Decisions, not Options][decisions], -the API is opinionated and designed for the most common use case. You may find -that what you're trying to do doesn't fit in with the REST philosophy, and -**that's fine**; the low-level `admin-ajax.php` handler is -[still available][admin-ajax] for you to use in these scenarios, and you can -always roll your own API. An example of an API which doesn't fit with this API -is the heartbeat API; heartbeats aren't inherently designed around resources, -but events, so are a poor fit for a REST API. - -That said, evaluate whether it's better to rethink your structure in terms of -resources, or to roll your own API. In most cases, thinking about data in terms -of resources rather than events or actions will not only fit better with the -API, but also make for cleaner and more maintainable code. For example, although -publishing a post is an action, it's equivalent to updating a post's status. - -Keep in mind that since the REST API is opinionated, you will have to work with -the API's structure rather than rolling your own. The API is strict about how -you talk to the client, and you should always use the built-in handlers rather -than rolling your own. This strict enforcement of data handling helps to make -the API more pluggable and extensible, plus ensures that you play nice with -other plugins. - -[decisions]: http://wordpress.org/about/philosophy/ -[admin-ajax]: http://codex.wordpress.org/AJAX_in_Plugins - - -Designing Your Entities ------------------------ -The first step you should take in working with the API is to think about the -data that you're working with. You should always think about the data in terms -of the clients that you're working with; it's your job to handle translation -back and forth. While it might seem easier to just return internal objects -directly, this may expose private data, or data that's harder to handle for -clients; posts internally translate WordPress's date format (YYYY-MM-DD -HH:MM:SS) to the standard RFC3339 which can be parsed by most date parsers. It's -worth your time to consider issues like this which can save clients hours of -work on their end. - -You should also consider how other plugins might extend your data, and design -for this. While things like `comment_status` could become a boolean field, -plugins might add custom statuses. On the other side of this, you should also -consider how these fields should be handled by clients. Custom field values that -a client doesn't understand should have a sensible default handling; for -example, the `comment_status` field is documented as the following: - -> Providers MAY use statuses other than "open" or "closed" to indicate other -> statuses. Consumers who encounter an unknown or missing comment status SHOULD -> treat it as "closed". - -This ensures that while clients may not know what a field value means, they can -still operate in a safe manner with the posts. - - -Designing Your Routes and Endpoints ------------------------------------ -After you've decided what your entities should look like, it's time to think -about how you want clients to access and interact with the data. Picking your -routes is an important part of this, and should be oriented around your data. -Collections of Entities should be a base route, with individual Entities as -subroutes of this. The following is a good general rule: - -* `/`: - * GET returns a collection - * POST adds a new item to the collection -* `//`: - * GET returns an entity - * PUT updates the entity - * DELETE deletes the entity - -(Again, this is the difference between routes and endpoints. There are two -routes here, but with two and three endpoints respectively. In general, a route -maps to an Entity/Collection, whereas an endpoint maps to an action on that -Entity/Collection.) - -Since your data is in your plugin, you should also prefix your routes. For -example, routes for Hello Dolly might look something like: - -* `/hello-dolly`: Get all lines from Hello Dolly -* `/hello-dolly/`: Get the nth line from Hello Dolly -* `/hello-dolly/random`: Get a random line from Hello Dolly - -If you're extending an existing route, you might want to use a flat style of -prefixing. Plugins like Akismet might implement something like: - -* `/posts//comments/akismet-recheck`: Recheck a post's comments for spam - -Note that this is somewhat against the previously discussed philosophy of -organising around resources; sometimes, you might want to have actions -available directly. These should always be a POST endpoint, since you're taking -action on the server in response. You should also avoid this where possible, -although there's no need to go overboard and create things like an Akismet Queue -resource just for a single action. - - -Organizing Your Endpoints -------------------------- -Now that you've got a roadmap for everything you'd like to implement, it's time -to start implementing it. - -The recommended way to organize your endpoints is to group related endpoints -together into a class, and extend the base classes built into the API. -Post-related endpoints should look to extend the `WP_JSON_CustomPostType` class, -and use the same method naming as it, falling back to it for the post -preparation and request handling. This will automatically register all the post -methods for their endpoints. - -Along these lines, keep your methods named as generically as possible; while -`MyPlugin_API_MyType::get_my_type_items()` might seem like a good name, it makes it -harder for other plugins to use; standardising on -`MyPlugin_API_MyType::get_posts()` with similar arguments to the parent is a -better idea and allows a nicer fall-through. - -You should also aim to keep these related endpoints modular, and make liberal -use of filters for this. As an example of this, the built-in Posts API is -filtered by the Media API to add featured image-related data, ensuring that -there's no dependencies between the two and either can be enabled independently. - - -Creating Your Endpoints ------------------------ -Before you actually write the code, there's some important things to know about -how you write the code. (Remember the opinions I mentioned before? This -is them.) - -Each endpoint should be written in the same way you'd write an internal function -for your plugin. That is, they take parameters, and return data. They also -*never* read directly from request globals (such as `$_GET` or `$_POST`), but -instead read them via the parameters to your function. The REST API serving code -automatically maps GET and POST parameters to your function's parameters (along -with some special request-related parameters). This concept might seem a little -strange at first, but it helps to ensure pluggability and standardisation of -errors. - -For example, an endpoint that takes a required `context` parameter, an optional -`type` parameter and uses the `X-WP-Example` header would look like this: - - function get_my_data( $context, $_headers, $type = 'my-default-value' ) { - if ( isset( $_headers['X-WP-EXAMPLE'] ) ) { - $my_header_value = $_headers['X-WP-EXAMPLE']; - } - - if ( $type !== 'my-default-value' && $type !== 'some-other-value' ) { - return new WP_Error( 'myplugin_mydata_invalid_type', __( 'Invalid type.' ), array( 'status' => 400 ) ); - } - - // ... - - return array( /* ... */ ); - } - -Note that this is intentionally the same way you'd write a function for internal -consumption; default parameter values are specified as default argument values, -errors are returned as a WP_Error object (with the special `status` field in the -data set to an appropriate HTTP status), and the data itself being returned from -the function. - -The following special values are also available, and can be trusted (always -internal data, not overridable by clients): - -* `_method`: The requested HTTP method (`GET`, `HEAD`, `POST`, `PUT`, `DELETE`) -* `_route`: The route followed to your endpoint (`/posts/(?P\d+)`) -* `_path`: The actual path that matches your route (`/posts/1`) -* `_headers`: An associative array of header names to values. Names are always - uppercased (HTTP header names are case-insensitive) -* `_files`: An associative array of upload file data, in the same format - as `$_FILES` - -The special `data` parameter is also available if your endpoint is set to -receive JSON or raw data. Note that this can be given by clients either via the -HTTP body or via the `data` query parameter, which is intentional for backwards -compatibility. That is, the following requests are the same: - - Content-Type: application/json; charset=utf-8 - - { - "foo": "bar", - "hello": "dolly" - } - - ---- - - Content-Type: application/x-www-form-urlencoded - - data[foo]=bar&data[hello]=dolly - -The return value of your endpoints should be an error object with the status -field set to an [appropriate value][http-status-codes], or the relevant data. -The data itself can be any JSON-serializable value: an integer, string, array, -object or null. In practice, this is almost always an array (for Collections) or -an object (for Entities). - -Return headers can be set by returning an instance of `WP_JSON_Response`, which -includes a `header()` method, as well as similar helper methods -(`set_status()`, `link_header()`, `query_navigation_headers()`) called on the -response object, but should *never* be set via the direct `header()` or -`status_header()` functions. - -[http-status-codes]: http://en.wikipedia.org/wiki/List_of_HTTP_status_codes - - -Registering Your Endpoints --------------------------- -Now that we've done the bulk of the work, it's time to tell WordPress that we -have an API to register. If you're following the same structure as the API's -built-in types, your registration code should look something like this: - - function myplugin_api_init() { - global $myplugin_api_mytype; - - $myplugin_api_mytype = new MyPlugin_API_MyType(); - add_filter( 'json_endpoints', array( $myplugin_api_mytype, 'register_routes' ) ); - } - add_action( 'wp_json_server_before_serve', 'myplugin_api_init' ); - - class MyPlugin_API_MyType { - public function register_routes( $routes ) { - $routes['/myplugin/mytypeitems'] = array( - array( array( $this, 'get_posts'), WP_JSON_Server::READABLE ), - array( array( $this, 'new_post'), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), - ); - $routes['/myplugin/mytypeitems/(?P\d+)'] = array( - array( array( $this, 'get_post'), WP_JSON_Server::READABLE ), - array( array( $this, 'edit_post'), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), - array( array( $this, 'delete_post'), WP_JSON_Server::DELETABLE ), - ); - - // Add more custom routes here - - return $routes; - } - - // ... - } - -You will need to implement the `get_post`, `edit_post`, `get_posts`, and -`new_post` methods within your new class. Take a look at the `WP_JSON_Posts` -class to see examples of how these methods can be written. - -Alternatively, use the custom post type base class, which will handle the -hooking and more for you: - - // main.php - function myplugin_api_init( $server ) { - global $myplugin_api_mytype; - - require_once dirname( __FILE__ ) . '/class-myplugin-api-mytype.php'; - $myplugin_api_mytype = new MyPlugin_API_MyType( $server ); - $myplugin->register_filters(); - } - add_action( 'wp_json_server_before_serve', 'myplugin_api_init' ); - - // class-myplugin-api-mytype.php - class MyPlugin_API_MyType extends WP_JSON_CustomPostType { - protected $base = '/myplugin/mytypeitems'; - protected $type = 'myplugin-mytype'; - - public function register_routes( $routes ) { - $routes = parent::register_routes( $routes ); - // $routes = parent::register_revision_routes( $routes ); - // $routes = parent::register_comment_routes( $routes ); - - // Add more custom routes here - - return $routes; - } - - // ... - } - -(Note that this CPT base class handles other things as well, including strict -post type checking and correcting URLs.) - -It is important that this class lives in a separate file that is only included -on a WP API hook, as your plugin may load before the WP API plugin. If you get -errors about the `WP_JSON_CustomPostType` class not being loaded, this is -the reason. - -The data passed in and returned by the `json_endpoints` filter should be in the -format of an array containing a map of route regular expression to endpoint -lists. An endpoint list is a potentially unlimited array of endpoints' data. -Endpoint data is a two-element array, with the first element being the endpoint -callback, and the second element specifying options. In other words, the format -is: - - $routes['regex expression'] = array( - // one or more: - array( - 'callback_function', - 0 // bitwise options flag - ) - ); - -(Where possible, use named parameters of the form `(?P...)` for variables -in your route, as these will be automatically replaced with a more friendly -version in the index.) - -The available bitwise options are: - -* `WP_JSON_Server::READABLE`: Read endpoint (responds to GET) -* `WP_JSON_Server::CREATABLE`: Creation endpoint (responds to POST) -* `WP_JSON_Server::EDITABLE`: Edit endpoint (responds to POST/PUT/PATCH) -* `WP_JSON_Server::DELETABLE`: Deletion endpoint (responds to DELETE) -* `WP_JSON_Server::ALLMETHODS`: Generic endpoint (responds to - GET/POST/PUT/PATCH/DELETE) -* `WP_JSON_Server::ACCEPT_RAW`: Accepts raw data in the HTTP body -* `WP_JSON_Server::ACCEPT_JSON`: Accepts JSON data in the HTTP body - (automatically deserialized by the server) -* `WP_JSON_Server::HIDDEN_ENDPOINT`: Hide the endpoint from the index - -Once you've done this, you should now go and check that your route is appearing -in the index. Whip out a HTTP client and test out that your API endpoints are -working correctly. - - -Next Steps ----------- -You should now have a working API built with the REST API. Congratulations! -You now have a mastery of how the API works, so get out there and start -expanding your API. You should consider contributing back to the core API and -improving the built-in APIs. - -* [API Philosophy][]: Learn more about the design of the API and the decisions - made in the core. -* [Schema][]: Read up on the schema for the core API and use it to inspire your - own entity design. -* [Internal Implementation][]: Learn about how the REST server works internally. - -[API Philosophy]: ../internals/philosophy.md -[Schema]: ../schema.md -[Internal Implementation]: ../internals/implementation.md diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md deleted file mode 100644 index b0aaacefe6..0000000000 --- a/docs/guides/getting-started.md +++ /dev/null @@ -1,473 +0,0 @@ -Getting Started -=============== - -Hi! If you're reading this, you're probably wondering how to get started using -the REST API. In that case, you've come to the right place! I'm about to show -you the basics of the API from the ground up. - -(If you're in the wrong place, there's not much I can do to help. Sorry!) - - -Before We Start ---------------- -To interact with the API, you're going to need some tools. The first of these -you need is a HTTP client/library. These examples use command-line cURL, but any -HTTP client would do here. - -The next tool you need is a JSON parser, and generator if you're sending data. -I'm going to do this by hand for the command-line examples, but it's recommended -to use a proper serializer/deserializer in your programming language of choice. -That said, it's not a bad idea to know how to read and write JSON by hand. - - -Checking for the API --------------------- -Before you start using the API, it's a good idea to check that the site itself -supports the API. WordPress is flexible enough to allow disabling the API or -parts thereof, and plugins may add or change parts of the API, so it's always -good to double-check this. - -The easiest way to check this is to send a HEAD request to any page on the site. -Any site with the API enabled will return a `Link` header pointing to the API, -with the link relation set to `https://github.com/WP-API/WP-API`. - -My test site for this documentation is set up at `http://example.com/`, so we -can find the API by sending a HEAD request to this URL: - - curl -I http://example.com/ - -(Uppercase `-I` here sends a HEAD request rather than a GET request, since we -only care about the headers here. I'll strip some irrelevant headers for -this documentation.) - -And we get back: - - HTTP/1.1 200 OK - X-Pingback: http://example.com/wp/xmlrpc.php - Link: ; rel="https://github.com/WP-API/WP-API" - -The `Link` header tells us that the base URL for the API is -`http://example.com/wp-json`. Any routes should be appended to this. For -example, the API index that we'll use in a second is available at the `/` route, -so we append this to the URL to get `http://example.com/wp-json/` -(note the trailing slash). - -For sites without pretty permalinks enabled, this will return something like -`http://example.com/?json_route=` instead; the route should again be appended -directly as a string, giving the API index at `http://example.com/?json_route=/` - -(You can also "discover" the API by looking for the HTML `` in the -`` with the same link relation, or in the RSD along with other WordPress -API information.) - -Checking the API Index ----------------------- -As our next command, let's go ahead and check the API index. The index tells us -what routes are available, and a short summary about the site. - -As we discovered previously, the index is available at the site's address with -`/wp-json/` on the end. Let's fire off the request: - - curl -i http://example.com/wp-json/ - -(By the way, `-i` tells cURL that we want to see the headers as well. As before, -I'll strip some irrelevant ones for this documentation.) - -And here's what we get back: - - HTTP/1.1 200 OK - Content-Type: application/json; charset=UTF-8 - -```json -{ - "name": "My WordPress Site", - "description": "Just another WordPress site", - "URL": "http:\/\/example.com", - "routes": { - "\/": { - "supports": [ - "HEAD", - "GET" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/" - } - }, - "\/posts": { - "supports": [ - "HEAD", - "GET", - "POST" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/posts" - }, - "accepts_json": true - }, - "\/posts\/": { - "supports": [ - "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "accepts_json": true - }, - "\/posts\/\/revisions": { - "supports": [ - "HEAD", - "GET" - ] - }, - "\/posts\/\/comments": { - "supports": [ - "HEAD", - "GET", - "POST" - ], - "accepts_json": true - }, - "\/posts\/\/comments\/": { - "supports": [ - "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "accepts_json": true - }, - "\/posts\/types": { - "supports": [ - "HEAD", - "GET" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/posts\/types" - } - }, - "\/posts\/types\/": { - "supports": [ - "HEAD", - "GET" - ] - }, - "\/posts\/statuses": { - "supports": [ - "HEAD", - "GET" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/posts\/statuses" - } - }, - "\/taxonomies": { - "supports": [ - "HEAD", - "GET" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/taxonomies" - } - }, - "\/taxonomies\/": { - "supports": [ - "HEAD", - "GET" - ] - }, - "\/taxonomies\/\/terms": { - "supports": [ - "HEAD", - "GET" - ] - }, - "\/taxonomies\/\/terms\/": { - "supports": [ - "HEAD", - "GET" - ] - }, - "\/users": { - "supports": [ - "HEAD", - "GET", - "POST" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/users" - }, - "accepts_json": true - }, - "\/users\/me": { - "supports": [ - "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/users\/me" - } - }, - "\/users\/": { - "supports": [ - "HEAD", - "GET", - "POST" - ], - "accepts_json": true - } - }, - "meta": { - "links": { - "help": "https:\/\/github.com\/WP-API\/WP-API", - "profile": "https:\/\/raw.github.com\/rmccue\/WP-API\/master\/docs\/schema.json" - } - } -} -``` - -Wow, that's a lot of data! Let's break it down. - -First, let's look at the headers. We get a 200 status code back, indicating that -we can successfully get the index. Note that it's possible for sites to disable -the API, which would give us a 404 error. You can check the returned body to -find out if the API exists at all; a disabled site will give you a JSON object -with the `json_disabled` error code, while a site without the API at all will -probably give you an error from the theme. - -Next, we can see the `name`, `description` and `URL` fields. These are intended -to be used if you want to show the site's name to users, such as in settings -dialogs. - -The `routes` object contains the meat of the body. This object has a bunch of -keys with "routes" (templates for creating URLs) pointing to objects containing -data (this data tells you about the "endpoint"). Each of these objects has at -least a `supports` key that contains a list of the HTTP methods supported. The -`meta` key has a hyperlink to the route, while the `accepts_json` key will also -be set and be `true` if you can POST/PUT JSON directly to the endpoint. - -You might notice that some of the routes have a `` part in them. This -is a variable part, and it tells you that you can replace it with something, -such as an object ID. - - -Routes vs Endpoints -------------------- -Quick note on terminology: a "route" is a URL that gets passed into the API; -these look like `/posts` or `/posts/2` and can be written in a generic form as -`/some/part/`. An "endpoint" on the other hand is the handler -that actually does something with your request. - -For example, the `/posts` route has two endpoints: the Retrieve Posts endpoint -to handle GET requests, and the Create Post endpoint to handle POST requests. - - -Meta Objects ------------- -You'll also see in the response that we have a `meta` object at the top level, -plus a bunch of smaller ones for the routes. Meta objects are mappings of [link -relations][] to their corresponding URL. These URLs are usually internal URLs to -help you browse the API, and can be used by clients embracing the [HATEOAS][] -methodology. - -[link relations]: http://www.iana.org/assignments/link-relations/link-relations.xhtml -[HATEOAS]: http://en.wikipedia.org/wiki/HATEOAS - -Here's some common relations you'll see: - -* `self`: The canonical URL for the resource. This is usually most helpful in - embedded objects (post parent, author, etc) -* `collection`: The URL for the collection containing the resource. The endpoint - at this route usually gives a list, of which, the current resource is a - member (for example, `/posts/1` would have a collection of `/posts`) -* `archive`: The URL for the collection owned by this resource (for example, - posts written by an author) -* `replies`: Responses to a resource (for example, comments on a post, replies - to a comment) - - -Getting Posts -------------- -Now that we understand some of the basics, let's have a look at the posts route. -All we need to do is send a GET request to the posts endpoint. - - curl -i http://example.com/wp-json/posts - -And this time, we get (again trimming headers, you'll have more than this): - - HTTP/1.1 200 OK - Last-Modified: Wed, 31 Oct 2012 18:26:17 GMT - Link: ; rel="item"; title="Hello world!" - -```json -[ - { - "ID": 1, - "title": "Hello world!", - "status": "publish", - "type": "post", - "author": { - "ID": 1, - "name": "admin", - "slug": "admin", - "URL": "", - "avatar": "http:\/\/0.gravatar.com\/avatar\/c57c8945079831fa3c19caef02e44614&d=404&r=G", - "meta": { - "links": { - "self": "http:\/\/example.com\/wp-json\/users\/1", - "archives": "http:\/\/example.com\/wp-json\/users\/1\/posts" - } - } - }, - "content": "Welcome to WordPress. This is your first post. Edit or delete it, then start blogging!", - "parent": 0, - "link": "http:\/\/example.com\/2012\/10\/31\/hello-world\/", - "date": "2012-10-31T18:26:17+10:00", - "modified": "2012-10-31T18:26:17+10:00", - "format": "standard", - "terms": { - "category": { - "ID": 1, - "name": "Uncategorized", - "slug": "uncategorized", - "group": 0, - "parent": 0, - "count": 1, - "meta": { - "links": { - "collection": "http:\/\/example.com\/wp-json\/taxonomy\/category", - "self": "http:\/\/example.com\/wp-json\/taxonomy\/category\/terms\/1" - } - } - } - }, - "meta": { - "links": { - "self": "http:\/\/example.com\/wp-json\/posts\/1", - "author": "http:\/\/example.com\/wp-json\/users\/1", - "collection": "http:\/\/example.com\/wp-json\/posts", - "replies": "http:\/\/example.com\/wp-json\/posts\/1\/comments", - "version-history": "http:\/\/example.com\/wp-json\/posts\/1\/revisions" - } - } - } -] -``` - -Hopefully this looks fairly self-explanatory. For a full look at all the fields -you can get back from this, take a look at [Working with Posts][] or the -[definition][schema] for the Post entity. - -You'll notice that in this case we're getting a list back with just a single -item, one Post. If you have two Posts here, you'll get back two, and so on, up -to 10. If there are more than 10, pagination will kick in and you'll have to -handle this in your client. - -Pagination details are given in the HTTP headers from the endpoint. The -`X-WP-Total` header has the total number of posts available to you, while the -`X-WP-TotalPages` header has the number of pages. While you can build the page -URL manually, you should use the `next` and `prev` links from the `Link` header -where possible. - -Example of pagination headers: - - X-WP-Total: 492 - X-WP-TotalPages: 50 - Link: ; rel="next", - ; rel="prev" - -If you want to grab a single post, you can instead send a GET request to the -post itself. You can grab the URL for this from the `meta.links.self` field, or -construct it yourself (`/posts/`): - - curl -i http://example.com/wp-json/posts/1 - - -Editing and Creating Posts --------------------------- -Just as we can use a GET request to get a post, we can use PUT to edit a post. -The easiest way to do this is to send a JSON body back to the server with just -the fields you want to change. [Authentication][auth] is **required** to edit -posts. For example, to edit the title and the modification date: - -```json -{ - "title": "Hello Updated World!", - "modified": "2013-04-01T14:00:00+10:00" -} -``` - -Save the data as "updated-post.json", then we can send this to the server with -the correct headers and authentication. We will provide our username and -password with HTTP Basic authentication, which requires the [Basic Auth plugin][basic-auth-plugin] -be installed: - - curl --data-binary "@updated-post.json" \ - -H "Content-Type: application/javascript" \ - --user admin:password \ - http://example.com/wp-json/posts/1 - -And we should get back a 200 status code, indicating that the post has been -updated, plus the updated Post in the body. - -Note that there are some fields we can't update; ID is an obvious example, but -others like timezone fields cannot be updated either. Check the [schema][] for -more details on this. - -Similarly to editing posts, you can create posts. [Authentication][auth] is -**required** to create posts. We can use the same data from before, but this -time, we POST it to the main posts route. Again, we are providing our username -and password using HTTP Basic authentication which requires the -[Basic Auth plugin][basic-auth-plugin] be installed: - - curl --data-binary "@updated-post.json" \ - -H "Content-Type: application/javascript" \ - --user admin:password \ - http://example.com/wp-json/posts - -We should get a similar response to the editing endpoint, but this time we get -a 201 Created status code, with a Location header telling us where to access the -post in future: - - HTTP/1.1 201 Created - Location: http://example.com/wp-json/posts/2 - -Finally, we can clean this post up and delete it by sending a DELETE request: - - curl -X DELETE --user admin:password http://example.com/wp-json/posts/2 - -In general, routes follow the same pattern: - -* `/`: - * GET returns a collection - * POST adds a new item to the collection -* `//`: - * GET returns an entity - * PUT updates the entity - * DELETE deletes the entity - -Note that by convention, the plural form of `` is used in the URL -(e.g. `posts` instead of `post`, `pages` instead of `page`). - -Next Steps ----------- -You should now be able to understand the basics of accessing and creating data -via the API, plus have the ability to apply this to posts. We've only really -touched on the post-related data so far, and there's plenty more to the API, so -get exploring! - -* [Working With Posts][]: Learn more about Posts and their data -* [Schema][schema]: View technical information on all the available data -* [Authentication][auth]: Explore authentication options - -[Working with Posts]: working-with-posts.md -[schema]: ../schema.md -[auth]: ../authentication.md -[basic-auth-plugin]: https://github.com/WP-API/Basic-Auth diff --git a/docs/guides/working-with-posts.md b/docs/guides/working-with-posts.md deleted file mode 100644 index 0d16603469..0000000000 --- a/docs/guides/working-with-posts.md +++ /dev/null @@ -1,287 +0,0 @@ -Working with Posts -================== -Back in the [Getting Started][] guide we used posts to demonstrate how to work -with the API, but only touched on some of the details. Let's take a more -detailed look at the Post API. - - -Before We Start ---------------- -This guide assumes you have a basic knowledge of the API, as well as the -prerequisites noted in the [Getting Started] guide. If you haven't already, make -sure you read that guide to ensure you know the basics. - -This guide also assumes that you know how to send requests given how to use -them, so the examples will be HTTP requests. I recommend reading the cURL manual -or using a higher level tool if you don't know how to wrangle cURL. - -The examples also pretend that your JSON base URL (`wp-json` in the main WP -directory) is located at `/`, which is probably not the case. For example, if -your base URL is `http://example.com/wp-json` and the example request is -`GET /posts`, you should should actually send the following: - - GET /wp-json/posts HTTP/1.1 - Host: example.com - -Higher level HTTP clients can usually handle this for you. - - -Post Entities and Collections ------------------------------ -As mentioned previously, we can send a GET request to the main posts route to -get posts: - - GET /posts HTTP/1.1 - -This returns a list of Post objects. From a technical point of view, each object -is called an Entity (in this case, Post Entities), while the endpoint returns a -Collection (here, a Post Collection). - -Post Entities have certain [defined properties][schema], although which are set -depend on the context. For example, with embedded entities certain properties -may have different values; while the post parent entity is embedded, the parent -field inside this is only the ID. - - -Collection Views ----------------- -Post Collections can be modified by using various query string parameters. One -that you may already know is the `page` parameter. This parameter is used for -pagination, and while you can set it manually, it's also sent via the Link -headers to point you to the next and previous pages. A `context` parameter is -also available that we'll talk about later. - -You can also change the post type via the `type` parameter. It's recommended -that you check the [available post types](#custom-post-types) before doing this, -as plugins may change what's available. - -The last parameter is the `filter` parameter. This gives you full access to the -[WP_Query][] parameters, to alter the query to your liking. Depending on the -level of access you have, not all parameters will be available, so check the -[schema][] for the available parameters. A good assumption to make is that -anything you can put in a query on the site itself (such as `?s=...` for -searches) will be available. You can specify filter parameters in a request -using [array-style URL formatting][]. - - -Creating and Editing Posts --------------------------- -The main posts route also has a creation endpoint: - - POST /posts HTTP/1.1 - Content-Type: application/json; charset=utf-8 - - { - "title": "My Post Title" - } - -Here we send a Post entity that we've created locally to the server. These Post -entities are the same as Post entities served up by the server, but excluding -some fields which are immutable. For example, the ID and timezone fields cannot -be changed. - -When editing posts, it's helpful to pass the `edit` context along with your -request. This gives you extra data useful for editing, such as the unfiltered -content and title. This is not required, however it is recommended, as the -normal content and title fields have been filtered to correct HTML (such as via -the `wpautop` function), making them less than optimal for editing. - -You can also pass an `If-Unmodified-Since` header when editing posts with the -previous modification date to ensure that you don't accidentally overwrite edits -made in the meantime. Note that while dates in the `modified` field of the post -are in [RFC3339][] format, this header requires the use of [RFC1123][] (similar -to [RFC822][]). (Sorry about that, but the HTTP standard requires it.) - -[RFC3339]: http://tools.ietf.org/html/rfc3339 -[RFC1123]: http://tools.ietf.org/html/rfc1123 -[RFC822]: http://tools.ietf.org/html/rfc822 - - -Custom Post Types ------------------ -Custom post types can be queried via the main post routes (`/posts` and -children) when they have been made public. The type can be set via the `type` -query parameter. - -Before working with custom post types, you should first check that the API has -support for the post type that you want to work with. This data is available via -the read-only APIs at `/posts/types`: - - GET /posts/types HTTP/1.1 - -This should return a list of the available types: - - { - "post": { - "name": "Posts", - "slug": "post", - "description": "", - "labels": { - "name": "Posts", - "singular_name": "Post", - "add_new": "Add New", - "add_new_item": "Add New Post", - "edit_item": "Edit Post", - "new_item": "New Post", - "view_item": "View Post", - "search_items": "Search Posts", - "not_found": "No posts found.", - "not_found_in_trash": "No posts found in Trash.", - "parent_item_colon": null, - "all_items": "All Posts", - "menu_name": "Posts", - "name_admin_bar": "Post" - }, - "queryable": true, - "searchable": true, - "hierarchical": false, - "meta": { - "links": { - "self": "http:\/\/example.com\/wp-json\/posts\/types\/post", - "archives": "http:\/\/example.com\/wp-json\/posts" - } - } - }, - "page": { - "name": "Pages", - "slug": "page", - "description": "", - "labels": { - "name": "Pages", - "singular_name": "Page", - "add_new": "Add New", - "add_new_item": "Add New Page", - "edit_item": "Edit Page", - "new_item": "New Page", - "view_item": "View Page", - "search_items": "Search Pages", - "not_found": "No pages found.", - "not_found_in_trash": "No pages found in Trash.", - "parent_item_colon": "Parent Page:", - "all_items": "All Pages", - "menu_name": "Pages", - "name_admin_bar": "Page" - }, - "queryable": false, - "searchable": true, - "hierarchical": true, - "meta": { - "links": { - "self": "http:\/\/example.com\/wp-json\/posts\/types\/page" - } - } - }, - "attachment": { - "name": "Media", - "slug": "attachment", - "description": "", - "labels": { - "name": "Media", - "singular_name": "Media", - "add_new": "Add New", - "add_new_item": "Add New Post", - "edit_item": "Edit Media", - "new_item": "New Post", - "view_item": "View Attachment Page", - "search_items": "Search Posts", - "not_found": "No posts found.", - "not_found_in_trash": "No posts found in Trash.", - "parent_item_colon": null, - "all_items": "Media", - "menu_name": "Media", - "name_admin_bar": "Media" - }, - "queryable": true, - "searchable": true, - "hierarchical": false, - "meta": { - "links": { - "self": "http:\/\/example.com\/wp-json\/posts\/types\/attachment", - "archives": "http:\/\/example.com\/wp-json\/posts?type=attachment" - } - } - } - } - -The `meta.links.archives` value gives a nicer way to access given post type -archives for HATEOAS supporters and should always be used rather than manually -setting the `type` parameter directly, as CPTs may create their own route -structure instead. The `labels` fields should also always be used when -displaying the field, as these are run through WordPress' translations. - -A similar API exists for post statuses at `/posts/statuses`: - - { - "publish": { - "name": "Published", - "slug": "publish", - "public": true, - "protected": false, - "private": false, - "queryable": true, - "show_in_list": true, - "meta": { - "archives": "http:\/\/example.com\/wp-json\/posts" - } - }, - "future": { - "name": "Scheduled", - "slug": "future", - "public": false, - "protected": true, - "private": false, - "queryable": false, - "show_in_list": true, - "meta": [ - ] - }, - "draft": { - "name": "Draft", - "slug": "draft", - "public": false, - "protected": true, - "private": false, - "queryable": false, - "show_in_list": true, - "meta": [ - ] - }, - "pending": { - "name": "Pending", - "slug": "pending", - "public": false, - "protected": true, - "private": false, - "queryable": false, - "show_in_list": true, - "meta": [ - ] - }, - "private": { - "name": "Private", - "slug": "private", - "public": false, - "protected": false, - "private": true, - "queryable": false, - "show_in_list": true, - "meta": [ - ] - } - } - - -Next Steps ----------- -You should now understand more advanced usage of the post-related APIs, and be -able to implement a fully compliant client for the API. You might now want to -take a look at the other APIs, or look at documentation on the specifics. - -* [Schema][schema]: Full documentation of every parameter for the APIs. -* [Extending the API][]: Create your own API endpoints. - -[Getting Started]: getting-started.md -[Extending the API]: extending.md -[schema]: ../schema.md -[WP_Query]: http://codex.wordpress.org/Class_Reference/WP_Query -[array-style URL formatting]: ../compatibility.md#inputting-data-as-an-array From 4b98fb1120a6bf04467d754dc5dd4bc10bc34367 Mon Sep 17 00:00:00 2001 From: Sarah Gooding Date: Mon, 23 Jun 2014 03:45:31 -0500 Subject: [PATCH 359/515] Updated documentation links in main docs folder Updated links to point to the wp-api.org site. --- docs/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index 0b220dedba..fdc2615f33 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,10 +16,10 @@ Dive in deeper into the [schema details][schema] to better understand the little details, or read about the [philosophy][] behind them. Read about the [implementation details][implementation] on how the API works internally. -[Getting Started]: guides/getting-started.md -[guides]: guides/README.md -[post-routes]: routes/posts.md -[media-routes]: routes/media.md +[Getting Started]: http://wp-api.org/getting-started.md +[guides]: http://wp-api.org/guides.html +[post-routes]: http://wp-api.org/#posts +[media-routes]: http://wp-api.org/#media [compatibility]: compatibility.md [schema]: schema.md [philosophy]: internals/philosophy.md From 4ad8a6a8661ff229f410872898ecc086329d7f0f Mon Sep 17 00:00:00 2001 From: JDGrimes Date: Mon, 23 Jun 2014 09:16:28 -0400 Subject: [PATCH 360/515] Add summary of the $context parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For the `get_post(s)` methods, I think only the `’view’` context is used internally, but I’ve referenced `’edit’` there as well. --- lib/class-wp-json-customposttype.php | 2 +- lib/class-wp-json-pages.php | 2 +- lib/class-wp-json-posts.php | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-json-customposttype.php b/lib/class-wp-json-customposttype.php index faf0a5573e..29fc7f9699 100644 --- a/lib/class-wp-json-customposttype.php +++ b/lib/class-wp-json-customposttype.php @@ -189,7 +189,7 @@ public function delete_post( $id, $force = false ) { * Prepare post data * * @param array $post The unprepared post data - * @param string $context + * @param string $context The context for the prepared post. (view|view-revision|edit|embed|single-parent) * @return array The prepared post data */ protected function prepare_post( $post, $context = 'view' ) { diff --git a/lib/class-wp-json-pages.php b/lib/class-wp-json-pages.php index a4b43d46cf..846ca697fb 100644 --- a/lib/class-wp-json-pages.php +++ b/lib/class-wp-json-pages.php @@ -103,7 +103,7 @@ public function delete_post_by_path( $path, $force = false ) { * Prepare post data * * @param array $post The unprepared post data - * @param string $context + * @param string $context The context for the prepared post. (view|view-revision|edit|embed|single-parent) * @return array The prepared post data */ protected function prepare_post( $post, $context = 'view' ) { diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 2c4be56ebc..27ce5e69a6 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -120,7 +120,7 @@ public function get_revisions( $id ) { * @see get_posts() for more on $filter values * * @param array $filter Parameters to pass through to `WP_Query` - * @param string $context + * @param string $context The context; 'view' (default) or 'edit'. * @param string|array $type Post type slug, or array of slugs * @param int $page Page number (1-indexed) * @return stdClass[] Collection of Post entities @@ -313,7 +313,7 @@ public function new_post( $data ) { * * @uses get_post() * @param int $id Post ID - * @param string $context + * @param string $context The context; 'view' (default) or 'edit'. * @return array Post entity */ public function get_post( $id, $context = 'view' ) { @@ -659,7 +659,7 @@ public function get_post_statuses() { * @access protected * * @param array $post The unprepared post data - * @param string $context The context for the prepared post. (view|view-revision|edit|embed) + * @param string $context The context for the prepared post. (view|view-revision|edit|embed|single-parent) * @return array The prepared post data */ protected function prepare_post( $post, $context = 'view' ) { From 18c05f219c8bfc559b3c32f3c0a445b2e69dc9b8 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 23 Jun 2014 19:27:47 -0500 Subject: [PATCH 361/515] Corrected cookie authentication docs example to match changes in js-client. --- docs/authentication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/authentication.md b/docs/authentication.md index c0051d524a..c6394fd1e2 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -32,14 +32,14 @@ POST data or in the query for GET requests), or via the `X-WP-Nonce` header. As an example, this is how the built-in Javascript client creates the nonce: ```php -wp_localize_script( 'wp-api', 'wpApiOptions', array( 'base' => json_url(), 'nonce' => wp_create_nonce( 'wp_json' ) ) ); +wp_localize_script( 'wp-api', 'WP_API_Settings', array( 'root' => esc_url_raw( get_json_url() ), 'nonce' => wp_create_nonce( 'wp_json' ) ) ); ``` This is then used in the base model: ```javascript options.beforeSend = function(xhr) { - xhr.setRequestHeader('X-WP-Nonce', wpApiOptions.nonce); + xhr.setRequestHeader('X-WP-Nonce', WP_API_Settings.nonce); if (beforeSend) { return beforeSend.apply(this, arguments); From a7993894698192d40129c8d592862d8b2c61c1ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Dini=C4=87?= Date: Wed, 25 Jun 2014 14:34:41 +0200 Subject: [PATCH 362/515] User's ID should alway be integer When user that doesn't have `list_users` capability request its own data using `/users/me`, they are redirected to `/users/`, which means ID is passed as a string to `WP_JSON_Users::get_user()`. As `get_current_user_id()` returns integer, and there is strict comparison, this will fail. User's ID should be converted to integer, or there shouldn't be strict comparison. --- lib/class-wp-json-users.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 7a9ff1e904..c9717a5ac0 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -123,6 +123,7 @@ public function get_current_user( $context = 'view' ) { * @return response */ public function get_user( $id, $context = 'view' ) { + $id = (int) $id; $current_user_id = get_current_user_id(); if ( $current_user_id !== $id && ! current_user_can( 'list_users' ) ) { From dcc8c44de90b9f0167440fac0b89fe367f9f613f Mon Sep 17 00:00:00 2001 From: Paul Kevan Date: Mon, 7 Jul 2014 08:59:56 +0100 Subject: [PATCH 363/515] Permissions to upload by post id changed - Now checks the post type of the post id being passed - Rename variable to make it clearer we are trying to upload to it (post_parent) --- lib/class-wp-json-media.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-media.php b/lib/class-wp-json-media.php index 57c3d3562f..1fecf420fd 100644 --- a/lib/class-wp-json-media.php +++ b/lib/class-wp-json-media.php @@ -189,7 +189,12 @@ public function delete_post( $id, $force = false ) { public function upload_attachment( $_files, $_headers, $post_id = 0 ) { $post_type = get_post_type_object( 'attachment' ); - $post_type_post = get_post_type_object( 'post' ); + + if ( $post_id == 0 ) { + $post_parent_type = get_post_type_object( 'post' ); + } else { + $post_parent_type = get_post_type_object( get_post_type( $post_id ) ); + } // Make sure we have an int or 0 $post_id = (int) $post_id; @@ -204,7 +209,7 @@ public function upload_attachment( $_files, $_headers, $post_id = 0 ) { } // If a user is trying to attach to a post make sure they have permissions. Bail early if post_id is not being passed - if ( $post_id !== 0 && ! current_user_can( $post_type_post->cap->edit_post, $post_id ) ) { + if ( $post_id !== 0 && ! current_user_can( $post_parent_type->cap->edit_post, $post_id ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => 401 ) ); } From 02582f6fb24748af7248d6e9cc78c19f315d6e4f Mon Sep 17 00:00:00 2001 From: Sarah Gooding Date: Fri, 11 Jul 2014 23:09:38 -0500 Subject: [PATCH 364/515] Moved routes docs into 1 file --- docs/routes/media.md | 53 ------ docs/routes/{posts.md => routes.md} | 284 ++++++++++++++++++++++++++-- docs/routes/taxonomies.md | 42 ---- docs/routes/users.md | 159 ---------------- 4 files changed, 272 insertions(+), 266 deletions(-) delete mode 100644 docs/routes/media.md rename docs/routes/{posts.md => routes.md} (59%) delete mode 100644 docs/routes/taxonomies.md delete mode 100644 docs/routes/users.md diff --git a/docs/routes/media.md b/docs/routes/media.md deleted file mode 100644 index de462e0c0f..0000000000 --- a/docs/routes/media.md +++ /dev/null @@ -1,53 +0,0 @@ -Media -===== - - -Create an Attachment --------------------- -The Create Attachment endpoint is used to create the raw data for an attachment. -This is a binary object (blob), such as image data or a video. - - POST /media - -### Input -The attachment creation endpoint can accept data in two forms. - -The primary input method accepts raw data POSTed with the corresponding content -type set via the `Content-Type` HTTP header. This is the preferred submission -method. - -The secondary input method accepts data POSTed via `multipart/form-data`, as per -[RFC 2388][]. The uploaded file should be submitted with the name field set to -"file", and the filename field set to the relevant filename for the file. - -In addition, a `Content-MD5` header can be set with the MD5 hash of the file, to -enable the server to check for consistency errors. If the supplied hash does not -match the hash calculated on the server, a 412 Precondition Failed header will -be issued. - -[RFC 2388]: http://tools.ietf.org/html/rfc2388 - -### Response -On a successful creation, a 201 Created status is given, indicating that the -attachment has been created. The attachment is available canonically from the -URL specified in the Location header. - -The new Attachment entity is also returned in the body for convienience. - - -Get Attachments ---------------- -The Attachments endpoint returns an Attachment collection containing a subset of -the site's attachments. - -This endpoint is an extended version of the Post retrieval endpoint. - - GET /media - -### Input -#### `fields` -... - -### Response -The response is an Attachment entity containing the requested Attachment if -available. diff --git a/docs/routes/posts.md b/docs/routes/routes.md similarity index 59% rename from docs/routes/posts.md rename to docs/routes/routes.md index 6467998731..0bd3ba3cf2 100644 --- a/docs/routes/posts.md +++ b/docs/routes/routes.md @@ -4,7 +4,7 @@ Posts Create a Post ------------- - POST /posts + POST /posts ### Input The `data` parameter consists of the elements of the Post object to be @@ -16,12 +16,12 @@ That is, the following are equivalent: Content-Type: application/x-www-form-urlencoded - data[title]=Hello%20World!&data[content_raw]=Content&data[excerpt_raw]=Excerpt + data[title]=Hello%20World!&data[content_raw]=Content&data[excerpt_raw]=Excerpt Content-Type: application/json - {"title":"Hello World!","content_raw":"Content","excerpt_raw":"Excerpt"} + {"title":"Hello World!","content_raw":"Content","excerpt_raw":"Excerpt"} The `data` parameter should be an object containing the following key value pairs: @@ -81,7 +81,7 @@ Retrieve Posts The Posts endpoint returns a Post Collection containing a subset of the site's posts. - GET /posts + GET /posts ### Input #### `filter` @@ -129,7 +129,7 @@ available. Retrieve a Post --------------- - GET /posts/ + GET /posts/ ### Input #### `context` @@ -153,7 +153,7 @@ fields available on the Post depend on the `context` parameter. Edit a Post ----------- - PUT /posts/ + PUT /posts/ For compatibility reasons, this endpoint also accepts the POST and PATCH methods. Both of these methods have the same behaviour as using PUT. It is @@ -219,7 +219,7 @@ updated. The updated Post entity is returned in the body. Delete a Post ------------- - DELETE /posts/ + DELETE /posts/ ### Input #### `force` @@ -242,7 +242,7 @@ indicating that the post has been permanently deleted. Create Meta for a Post ------------------------ - POST /posts//meta + POST /posts//meta ### Input The supplied data should be a Meta object. This data can be submitted via a @@ -266,7 +266,7 @@ The new Meta entity is also returned in the body for convienience. Retrieve Meta for a Post ------------------------ - GET /posts//meta + GET /posts//meta ### Response The response is a Meta entity containing all the post_meta for the specified @@ -276,7 +276,7 @@ Post if available. Retrieve a Meta for a Post ------------------------ - GET /posts//meta/ + GET /posts//meta/ ### Response The response a Meta entity containing the post_meta for the specified Meta and @@ -286,7 +286,7 @@ Post if available. Edit a Meta for a Post ------------------------ - PUT /posts//meta/ + PUT /posts//meta/ ### Input The supplied data should be a Meta object. This data can be submitted via a @@ -306,9 +306,269 @@ been updated. The updated Meta entity is returned in the body. Delete a Meta for a Post ------------- - DELETE /posts//meta/ + DELETE /posts//meta/ ### Response On successful deletion, a 200 OK status code will be returned, indicating that the post_meta has been permanently deleted. + + +Media +===== + + +Create an Attachment +-------------------- +The Create Attachment endpoint is used to create the raw data for an attachment. +This is a binary object (blob), such as image data or a video. + + POST /media + +### Input +The attachment creation endpoint can accept data in two forms. + +The primary input method accepts raw data POSTed with the corresponding content +type set via the `Content-Type` HTTP header. This is the preferred submission +method. + +The secondary input method accepts data POSTed via `multipart/form-data`, as per +[RFC 2388][]. The uploaded file should be submitted with the name field set to +"file", and the filename field set to the relevant filename for the file. + +In addition, a `Content-MD5` header can be set with the MD5 hash of the file, to +enable the server to check for consistency errors. If the supplied hash does not +match the hash calculated on the server, a 412 Precondition Failed header will +be issued. + +[RFC 2388]: http://tools.ietf.org/html/rfc2388 + +### Response +On a successful creation, a 201 Created status is given, indicating that the +attachment has been created. The attachment is available canonically from the +URL specified in the Location header. + +The new Attachment entity is also returned in the body for convienience. + + +Get Attachments +--------------- +The Attachments endpoint returns an Attachment collection containing a subset of +the site's attachments. + +This endpoint is an extended version of the Post retrieval endpoint. + + GET /media + +### Input +#### `fields` +... + +### Response +The response is an Attachment entity containing the requested Attachment if +available. + + +Users +===== + + +Create a User +------------- + + POST /users + +### Input +The supplied data should be a User object. This data can be submitted via a +regular HTTP multipart body, with User values set as values to the `data` +parameter, or through a direct JSON body. + +That is, the following are equivalent: + +Content-Type: application/x-www-form-urlencoded + + data[username]=newuser&data[name]=New%20User&data[password]=secret + + +Content-Type: application/json + + {"username":"newuser","name":"New User","password":"secret"} + +### Response +On a successful creation, a 201 Created status is given, indicating that the +user has been created. The user is available canonically from the URL specified +in the Location header. + +The new User entity is also returned in the body for convienience. + + +Retrieve Users +-------------- +The Users endpoint returns a User Collection containing a subset of the site's +users. + + GET /users + +### Input +#### `filter` +The `filter` parameter controls the query parameters. It is essentially a subset +of the parameters available to [`WP_User_Query`](http://codex.wordpress.org/Class_Reference/WP_User_Query). + +The parameter should be an array of the following key/value pairs: + +* `number` - Number of users to retrieve, use `-1` for all users. Default + is set by the site. (integer) +* `offset` - Number of posts to skip. Default is 0. (integer) +* `orderby` - Parameter to search by, as per [`WP_User_Query`](https://codex.wordpress.org/Class_Reference/WP_User_Query#Order_.26_Orderby_Parameters). + Default is "user_login". (string) +* `order` - Order to sort by. Default is "ASC". (string, "ASC" or "DESC") +* `s` - Keyword to search for. (string) + +### Response +The response is a User Collection document containing the requested Users if +available. + + +Retrieve a User +--------------- + + GET /users/ + +### Input +#### `context` +The `context` parameter controls the format of the data to return. The following +contexts are available: + +* `view`: The default context. Gives the normal User entity. +* `edit`: Context used for extra fields relevant to updating a user. Includes + the `extra_capabilities` field; this field contains the capabilities assigned + to the user themselves, rather than those inherited from their roles. +* `embed`: Context used when embedding the response inside another (e.g. post + author). This is intended as a minimal subset of the user data to reduce + response size. Excludes `roles` and `capabilities`. + +Default is "view". (string) + +### Response +The response is a User entity containing the requested User if available. The +fields available on the User depend on the `context` parameter. + + +Retrieve Current User +------------- + + GET /users/me + +This endpoint offers a permalink to get the current user, without needing to +know the user's ID. + +### Input +#### `context` +The `context` parameter controls the format of the data to return. See the +Retrieve a User endpoint for available contexts. + +Default is "view". (string) + +### Response +If the client is currently logged in, a 302 Found status is given. The User is +available canonically from the URL specified in the Location header. + +The User entity containing the current User is also returned in the body for +convenience. The fields available on the User depend on the `context` parameter. + +If the client is not logged in, a 401 Unauthorized status is given. + + +Edit a User +----------- + + PUT /users/ + +For compatibility reasons, this endpoint also accepts the POST and PATCH +methods. Both of these methods have the same behaviour as using PUT. It is +recommended to use PUT if available to fit with REST convention. + +### Input +The supplied data should be a User object. This data can be submitted via a +regular HTTP multipart body, with User values set as values to the `data` +parameter, or through a direct JSON body. See the Create User endpoint for an +example. + +### Response +On a successful update, a 200 OK status is given, indicating the user has been +updated. The updated User entity is returned in the body. + + +Delete a User +------------- + + DELETE /users/ + +### Input +#### `force` +The `force` parameter controls whether the user is permanently deleted or not. +By default, this is set to false, indicating that the user will be sent to an +intermediate storage (such as the trash) allowing it to be restored later. If +set to true, the user will not be able to be restored. + +Default is false. (boolean) + +#### `reassign` +The `reassign` parameter controls whether the deleted user's content is +reassigned to a new User or not. If set to `null`, the deleted user's content +will not be reassigned. + +Default is null. (integer) + + +### Response +On successful deletion, a 202 Accepted status code will be returned, indicating +that the user has been moved to the trash for permanent deletion at a +later date. + +If force was set to true, a 200 OK status code will be returned instead, +indicating that the user has been permanently deleted. + + +Taxonomies +========== + + +Retrieve All Taxonomies +----------------------- +The Taxonomies endpoint returns a collection containing objects for each of the +site's registered taxonomies. + + GET /taxonomies + + +### Response +The response is a collection document containing all registered taxonomies. + + +Retrieve a Taxonomy +------------------- + + GET /taxonomy/ + +### Response +The response is a Taxonomy entity containing the requested Taxonomy, if available. + + +Retrieve Terms for a Taxonomy +----------------------------- + + GET /taxonomy//terms + +### Response +The response is a collection of taxonomy terms for the specified Taxonomy, if +available. + +Retrieve a Taxonomy Term +------------------------ + + GET /taxonomy//terms/ + +### Response +The response is a Taxonomy entity object containing the Taxonomy with the +requested ID, if available. diff --git a/docs/routes/taxonomies.md b/docs/routes/taxonomies.md deleted file mode 100644 index 522e83caa9..0000000000 --- a/docs/routes/taxonomies.md +++ /dev/null @@ -1,42 +0,0 @@ -Taxonomies -========== - - -Retrieve All Taxonomies ------------------------ -The Taxonomies endpoint returns a collection containing objects for each of the -site's registered taxonomies. - - GET /taxonomies - - -### Response -The response is a collection document containing all registered taxonomies. - - -Retrieve a Taxonomy -------------------- - - GET /taxonomy/ - -### Response -The response is a Taxonomy entity containing the requested Taxonomy, if available. - - -Retrieve Terms for a Taxonomy ------------------------------ - - GET /taxonomy//terms - -### Response -The response is a collection of taxonomy terms for the specified Taxonomy, if -available. - -Retrieve a Taxonomy Term ------------------------- - - GET /taxonomy//terms/ - -### Response -The response is a Taxonomy entity object containing the Taxonomy with the -requested ID, if available. diff --git a/docs/routes/users.md b/docs/routes/users.md deleted file mode 100644 index e93b566dae..0000000000 --- a/docs/routes/users.md +++ /dev/null @@ -1,159 +0,0 @@ -Users -===== - - -Create a User -------------- - - POST /users - -### Input -The supplied data should be a User object. This data can be submitted via a -regular HTTP multipart body, with User values set as values to the `data` -parameter, or through a direct JSON body. - -That is, the following are equivalent: - -Content-Type: application/x-www-form-urlencoded - - data[username]=newuser&data[name]=New%20User&data[password]=secret - - -Content-Type: application/json - - {"username":"newuser","name":"New User","password":"secret"} - -### Response -On a successful creation, a 201 Created status is given, indicating that the -user has been created. The user is available canonically from the URL specified -in the Location header. - -The new User entity is also returned in the body for convienience. - - -Retrieve Users --------------- -The Users endpoint returns a User Collection containing a subset of the site's -users. - - GET /users - -### Input -#### `filter` -The `filter` parameter controls the query parameters. It is essentially a subset -of the parameters available to [`WP_User_Query`](http://codex.wordpress.org/Class_Reference/WP_User_Query). - -The parameter should be an array of the following key/value pairs: - -* `number` - Number of users to retrieve, use `-1` for all users. Default - is set by the site. (integer) -* `offset` - Number of posts to skip. Default is 0. (integer) -* `orderby` - Parameter to search by, as per [`WP_User_Query`](https://codex.wordpress.org/Class_Reference/WP_User_Query#Order_.26_Orderby_Parameters). - Default is "user_login". (string) -* `order` - Order to sort by. Default is "ASC". (string, "ASC" or "DESC") -* `s` - Keyword to search for. (string) - -### Response -The response is a User Collection document containing the requested Users if -available. - - -Retrieve a User ---------------- - - GET /users/ - -### Input -#### `context` -The `context` parameter controls the format of the data to return. The following -contexts are available: - -* `view`: The default context. Gives the normal User entity. -* `edit`: Context used for extra fields relevant to updating a user. Includes - the `extra_capabilities` field; this field contains the capabilities assigned - to the user themselves, rather than those inherited from their roles. -* `embed`: Context used when embedding the response inside another (e.g. post - author). This is intended as a minimal subset of the user data to reduce - response size. Excludes `roles` and `capabilities`. - -Default is "view". (string) - -### Response -The response is a User entity containing the requested User if available. The -fields available on the User depend on the `context` parameter. - - -Retrieve Current User -------------- - - GET /users/me - -This endpoint offers a permalink to get the current user, without needing to -know the user's ID. - -### Input -#### `context` -The `context` parameter controls the format of the data to return. See the -Retrieve a User endpoint for available contexts. - -Default is "view". (string) - -### Response -If the client is currently logged in, a 302 Found status is given. The User is -available canonically from the URL specified in the Location header. - -The User entity containing the current User is also returned in the body for -convenience. The fields available on the User depend on the `context` parameter. - -If the client is not logged in, a 401 Unauthorized status is given. - - -Edit a User ------------ - - PUT /users/ - -For compatibility reasons, this endpoint also accepts the POST and PATCH -methods. Both of these methods have the same behaviour as using PUT. It is -recommended to use PUT if available to fit with REST convention. - -### Input -The supplied data should be a User object. This data can be submitted via a -regular HTTP multipart body, with User values set as values to the `data` -parameter, or through a direct JSON body. See the Create User endpoint for an -example. - -### Response -On a successful update, a 200 OK status is given, indicating the user has been -updated. The updated User entity is returned in the body. - - -Delete a User -------------- - - DELETE /users/ - -### Input -#### `force` -The `force` parameter controls whether the user is permanently deleted or not. -By default, this is set to false, indicating that the user will be sent to an -intermediate storage (such as the trash) allowing it to be restored later. If -set to true, the user will not be able to be restored. - -Default is false. (boolean) - -#### `reassign` -The `reassign` parameter controls whether the deleted user's content is -reassigned to a new User or not. If set to `null`, the deleted user's content -will not be reassigned. - -Default is null. (integer) - - -### Response -On successful deletion, a 202 Accepted status code will be returned, indicating -that the user has been moved to the trash for permanent deletion at a -later date. - -If force was set to true, a 200 OK status code will be returned instead, -indicating that the user has been permanently deleted. From 4aa378b346d289607ad4d2e3a32da6b244cadc5a Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Sat, 12 Jul 2014 12:49:24 -0600 Subject: [PATCH 365/515] More inline documentation improvements for plugin.php. --- plugin.php | 117 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 71 insertions(+), 46 deletions(-) diff --git a/plugin.php b/plugin.php index a32c08ffd1..6754e8dc93 100644 --- a/plugin.php +++ b/plugin.php @@ -35,7 +35,9 @@ include_once( dirname( __FILE__ ) . '/lib/class-wp-json-taxonomies.php' ); /** - * Register our rewrite rules for the API + * Register rewrite rules for the API. + * + * @global WP $wp */ function json_api_init() { json_api_register_rewrites(); @@ -71,6 +73,13 @@ function json_api_maybe_flush_rewrites() { * Register the default JSON API filters. * * @internal This will live in default-filters.php + * + * @global $wp_json_posts + * @global $wp_json_pages + * @global $wp_json_media + * @global $wp_json_taxonomies + * + * @param WP_JSON_ResponseHandler $server Server object. */ function json_api_default_filters( $server ) { global $wp_json_posts, $wp_json_pages, $wp_json_media, $wp_json_taxonomies; @@ -80,7 +89,7 @@ function json_api_default_filters( $server ) { add_filter( 'json_endpoints', array( $wp_json_posts, 'register_routes' ), 0 ); add_filter( 'json_prepare_taxonomy', array( $wp_json_posts, 'add_post_type_data' ), 10, 3 ); - // Users + // Users. $wp_json_users = new WP_JSON_Users( $server ); add_filter( 'json_endpoints', array( $wp_json_users, 'register_routes' ), 0 ); add_filter( 'json_prepare_post', array( $wp_json_users, 'add_post_author_data' ), 10, 3 ); @@ -98,17 +107,17 @@ function json_api_default_filters( $server ) { add_filter( 'json_insert_post', array( $wp_json_media, 'attach_thumbnail' ), 10, 3 ); add_filter( 'json_post_type_data', array( $wp_json_media, 'type_archive_link' ), 10, 2 ); - // Posts + // Posts. $wp_json_taxonomies = new WP_JSON_Taxonomies( $server ); add_filter( 'json_endpoints', array( $wp_json_taxonomies, 'register_routes' ), 2 ); add_filter( 'json_post_type_data', array( $wp_json_taxonomies, 'add_taxonomy_data' ), 10, 3 ); add_filter( 'json_prepare_post', array( $wp_json_taxonomies, 'add_term_data' ), 10, 3 ); - // Deprecated reporting - add_action( 'deprecated_function_run', 'json_handle_deprecated_function', 10, 3 ); - add_filter( 'deprecated_function_trigger_error', '__return_false' ); - add_action( 'deprecated_argument_run', 'json_handle_deprecated_argument', 10, 3 ); - add_filter( 'deprecated_argument_trigger_error', '__return_false' ); + // Deprecated reporting. + add_action( 'deprecated_function_run', 'json_handle_deprecated_function', 10, 3 ); + add_filter( 'deprecated_function_trigger_error', '__return_false' ); + add_action( 'deprecated_argument_run', 'json_handle_deprecated_argument', 10, 3 ); + add_filter( 'deprecated_argument_trigger_error', '__return_false' ); } add_action( 'wp_json_server_before_serve', 'json_api_default_filters', 10, 1 ); @@ -146,7 +155,7 @@ function json_api_loaded() { $wp_json_server = new $wp_json_server_class; /** - * Prepare to serve an API request. + * Fires when preparing to serve an API request. * * Endpoint objects should be created and register their hooks on this * action rather than another action to ensure they're only loaded when @@ -156,10 +165,10 @@ function json_api_loaded() { */ do_action( 'wp_json_server_before_serve', $wp_json_server ); - // Fire off the request + // Fire off the request. $wp_json_server->serve_request( $GLOBALS['wp']->query_vars['json_route'] ); - // Finish off our request. + // We're done. die(); } add_action( 'template_redirect', 'json_api_loaded', -100 ); @@ -211,7 +220,9 @@ function json_api_deactivation( $network_wide ) { register_deactivation_hook( __FILE__, 'json_api_deactivation' ); /** - * Register our API Javascript helpers + * Register API Javascript helpers. + * + * @see wp_register_scripts() */ function json_register_scripts() { wp_register_script( 'wp-api', 'http://wp-api.github.io/client-js/build/js/wp-api.js', array( 'jquery', 'backbone', 'underscore' ), '1.1', true ); @@ -222,7 +233,7 @@ function json_register_scripts() { add_action( 'wp_enqueue_scripts', 'json_register_scripts', -100 ); /** - * Add the API URL to the WP RSD endpoint + * Add the API URL to the WP RSD endpoint. */ function json_output_rsd() { ?> @@ -232,7 +243,9 @@ function json_output_rsd() { add_action( 'xmlrpc_rsd_apis', 'json_output_rsd' ); /** - * Output API link tag into page header + * Output API link tag into page header. + * + * @see get_json_url() */ function json_output_link_wp_head() { $api_root = get_json_url(); @@ -246,7 +259,7 @@ function json_output_link_wp_head() { add_action( 'wp_head', 'json_output_link_wp_head', 10, 0 ); /** - * Send a Link header for the API + * Send a Link header for the API. */ function json_output_link_header() { if ( headers_sent() ) { @@ -264,11 +277,13 @@ function json_output_link_header() { add_action( 'template_redirect', 'json_output_link_header', 11, 0 ); /** - * Add `show_in_json` {@see register_post_type} argument. + * Add 'show_in_json' {@see register_post_type()} argument. * - * Adds the `show_in_json` post type argument to {@see register_post_type}. + * Adds the 'show_in_json' post type argument to {@see register_post_type()}. * This value controls whether the post type is available via API endpoints, - * and defaults to the value of `publicly_queryable` + * and defaults to the value of $publicly_queryable. + * + * @global $wp_post_types Post types list. * * @param string $post_type Post type to register. * @param stdClass $args Post type arguments. @@ -288,6 +303,7 @@ function json_register_post_type( $post_type, $args ) { $type->show_in_json = true; } + // Default to the value of $publicly_queryable. if ( ! isset( $type->show_in_json ) ) { $type->show_in_json = $type->publicly_queryable; } @@ -301,10 +317,13 @@ function json_register_post_type( $post_type, $args ) { * for logged in users. However, the API has to check nonces * for each request to ensure users are not vulnerable to CSRF. * + * @global $wp_json_auth_cookie + * * @param WP_Error|mixed $result Error from another authentication handler, * null if we should handle it, or another * value if not - * @return WP_Error|mixed|boolean + * @return WP_Error|mixed|bool WP_Error if the cookie is invalid, the $result, + * otherwise true. */ function json_cookie_check_errors( $result ) { if ( ! empty( $result ) ) { @@ -314,7 +333,7 @@ function json_cookie_check_errors( $result ) { global $wp_json_auth_cookie; /* - * Are we using cookie authentication? (If we get an auth + * Is cookie authentication being used? (If we get an auth * error, but we're still logged in, another authentication * must have been used.) */ @@ -322,7 +341,7 @@ function json_cookie_check_errors( $result ) { return $result; } - // Do we have a nonce? + // Is there a nonce? $nonce = null; if ( isset( $_REQUEST['_wp_json_nonce'] ) ) { $nonce = $_REQUEST['_wp_json_nonce']; @@ -351,6 +370,9 @@ function json_cookie_check_errors( $result ) { * * Collects errors from {@see wp_validate_auth_cookie} for * use by {@see json_cookie_check_errors}. + * + * @see current_action() + * @global $wp_json_auth_cookie */ function json_cookie_collect_status() { global $wp_json_auth_cookie; @@ -381,7 +403,7 @@ function json_get_url_prefix() { * * @since 1.0 * - * @param string $prefix URL prefix. + * @param string $prefix URL prefix. Default 'wp-json'. */ return apply_filters( 'json_url_prefix', 'wp-json' ); } @@ -448,7 +470,8 @@ function json_url( $path = '', $scheme = 'json' ) { * immediately check for this value. * * @param WP_Error|WP_JSON_ResponseInterface|mixed $response Response to check. - * @return WP_Error|WP_JSON_ResponseInterface + * @return WP_Error|WP_JSON_ResponseInterface WP_Error if present, WP_JSON_ResponseInterface + * instance otherwise. */ function json_ensure_response( $response ) { if ( is_wp_error( $response ) ) { @@ -463,14 +486,14 @@ function json_ensure_response( $response ) { } /** - * Parse an RFC3339 timestamp into a DateTime + * Parse an RFC3339 timestamp into a DateTime. * - * @param string $date RFC3339 timestamp - * @param boolean $force_utc Force UTC timezone instead of using the timestamp's TZ? - * @return DateTime + * @param string $date RFC3339 timestamp. + * @param bool $force_utc Force UTC timezone instead of using the timestamp's TZ. + * @return DateTime DateTime instance. */ function json_parse_date( $date, $force_utc = false ) { - // Default timezone to the server's current one + // Default timezone to the server's current one. $timezone = json_get_timezone(); if ( $force_utc ) { @@ -478,7 +501,7 @@ function json_parse_date( $date, $force_utc = false ) { $timezone = new DateTimeZone( 'UTC' ); } - // Strip millisecond precision (a full stop followed by one or more digits) + // Strip millisecond precision (a full stop followed by one or more digits). if ( strpos( $date, '.' ) !== false ) { $date = preg_replace( '/\.\d+/', '', $date ); } @@ -488,11 +511,12 @@ function json_parse_date( $date, $force_utc = false ) { } /** - * Get a local date with its GMT equivalent, in MySQL datetime format + * Get a local date with its GMT equivalent, in MySQL datetime format. * - * @param string $date RFC3339 timestamp - * @param boolean $force_utc Should we force UTC timestamp? - * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s), null on failure + * @param string $date RFC3339 timestamp + * @param bool $force_utc Whether a UTC timestamp should be forced. + * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s), + * null on failure. */ function json_get_date_with_gmt( $date, $force_utc = false ) { $datetime = json_parse_date( $date, $force_utc ); @@ -516,12 +540,13 @@ function json_get_date_with_gmt( $date, $force_utc = false ) { * {@see get_avatar()} doesn't return just the URL, so we have to * extract it here. * - * @param string $email Email address - * @return string url for the user's avatar + * @param string $email Email address. + * @return string URL for the user's avatar, empty string otherwise. */ function json_get_avatar_url( $email ) { $avatar_html = get_avatar( $email ); - // strip the avatar url from the get_avatar img tag. + + // Strip the avatar url from the get_avatar img tag. preg_match('/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches); if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) { @@ -532,9 +557,9 @@ function json_get_avatar_url( $email ) { } /** - * Get the timezone object for the site + * Get the timezone object for the site. * - * @return DateTimeZone + * @return DateTimeZone DateTimeZone instance. */ function json_get_timezone() { static $zone = null; @@ -562,11 +587,11 @@ function json_get_timezone() { } /** - * Handle `_deprecated_function` errors + * Handle {@see _deprecated_function()} errors. * - * @param string $function - * @param string $replacement - * @param string $version + * @param string $function Function name. + * @param string $replacement Replacement function name. + * @param string $version Version. */ function json_handle_deprecated_function( $function, $replacement, $version ) { if ( ! empty( $replacement ) ) { @@ -580,11 +605,11 @@ function json_handle_deprecated_function( $function, $replacement, $version ) { } /** - * Handle `_deprecated_function` errors + * Handle {@see _deprecated_function} errors. * - * @param string $function - * @param string $replacement - * @param string $version + * @param string $function Function name. + * @param string $replacement Replacement function name. + * @param string $version Version. */ function json_handle_deprecated_argument( $function, $message, $version ) { if ( ! empty( $message ) ) { From cd2891920438079757a37b0bf60c51ab5f85bcfb Mon Sep 17 00:00:00 2001 From: DrewAPicture Date: Sat, 12 Jul 2014 13:19:43 -0600 Subject: [PATCH 366/515] Some globals definitions. --- plugin.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugin.php b/plugin.php index 6754e8dc93..b5ed901a1b 100644 --- a/plugin.php +++ b/plugin.php @@ -37,7 +37,7 @@ /** * Register rewrite rules for the API. * - * @global WP $wp + * @global WP $wp Current WordPress environment instance. */ function json_api_init() { json_api_register_rewrites(); @@ -74,10 +74,10 @@ function json_api_maybe_flush_rewrites() { * * @internal This will live in default-filters.php * - * @global $wp_json_posts - * @global $wp_json_pages - * @global $wp_json_media - * @global $wp_json_taxonomies + * @global WP_JSON_Posts $wp_json_posts + * @global WP_JSON_Pages $wp_json_pages + * @global WP_JSON_Media $wp_json_media + * @global WP_JSON_Taxonomies $wp_json_taxonomies * * @param WP_JSON_ResponseHandler $server Server object. */ @@ -283,7 +283,7 @@ function json_output_link_header() { * This value controls whether the post type is available via API endpoints, * and defaults to the value of $publicly_queryable. * - * @global $wp_post_types Post types list. + * @global array $wp_post_types Post types list. * * @param string $post_type Post type to register. * @param stdClass $args Post type arguments. @@ -317,7 +317,7 @@ function json_register_post_type( $post_type, $args ) { * for logged in users. However, the API has to check nonces * for each request to ensure users are not vulnerable to CSRF. * - * @global $wp_json_auth_cookie + * @global mixed $wp_json_auth_cookie * * @param WP_Error|mixed $result Error from another authentication handler, * null if we should handle it, or another @@ -372,7 +372,7 @@ function json_cookie_check_errors( $result ) { * use by {@see json_cookie_check_errors}. * * @see current_action() - * @global $wp_json_auth_cookie + * @global mixed $wp_json_auth_cookie */ function json_cookie_collect_status() { global $wp_json_auth_cookie; From 59a2c4d63289645417c27870ac5b16c12fd86321 Mon Sep 17 00:00:00 2001 From: Sarah Gooding Date: Sat, 12 Jul 2014 16:36:04 -0500 Subject: [PATCH 367/515] Fixed broken Getting Started guide link --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index fdc2615f33..62eab164df 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,7 +16,7 @@ Dive in deeper into the [schema details][schema] to better understand the little details, or read about the [philosophy][] behind them. Read about the [implementation details][implementation] on how the API works internally. -[Getting Started]: http://wp-api.org/getting-started.md +[Getting Started]: http://wp-api.org/guides.html#getting-started [guides]: http://wp-api.org/guides.html [post-routes]: http://wp-api.org/#posts [media-routes]: http://wp-api.org/#media From b40e19495238eb77d5b4bfac07719711f1872ab5 Mon Sep 17 00:00:00 2001 From: David Hayes Date: Tue, 15 Jul 2014 15:54:25 -0600 Subject: [PATCH 368/515] Update taxonomies.md Spent five minutes confused because the documentation here is out of sync as all of these are now seem to be under the "taxonomies" slug where the documentation implied the singulars were under "taxonomy". --- docs/routes/taxonomies.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/routes/taxonomies.md b/docs/routes/taxonomies.md index 522e83caa9..486f995805 100644 --- a/docs/routes/taxonomies.md +++ b/docs/routes/taxonomies.md @@ -17,7 +17,7 @@ The response is a collection document containing all registered taxonomies. Retrieve a Taxonomy ------------------- - GET /taxonomy/ + GET /taxonomies/ ### Response The response is a Taxonomy entity containing the requested Taxonomy, if available. @@ -26,7 +26,7 @@ The response is a Taxonomy entity containing the requested Taxonomy, if availabl Retrieve Terms for a Taxonomy ----------------------------- - GET /taxonomy//terms + GET /taxonomies//terms ### Response The response is a collection of taxonomy terms for the specified Taxonomy, if @@ -35,7 +35,7 @@ available. Retrieve a Taxonomy Term ------------------------ - GET /taxonomy//terms/ + GET /taxonomies//terms/ ### Response The response is a Taxonomy entity object containing the Taxonomy with the From f292492da97cd2e1081337e8c31156f75b5d056d Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 16 Jul 2014 15:58:00 +1000 Subject: [PATCH 369/515] Mitigate possible Flash CSRF vulnerability See http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ Props @iandunn for disclosing this issue to the team. --- lib/class-wp-json-server.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index bd186b9586..ae2b76bc12 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -182,6 +182,10 @@ protected function json_error( $code, $message, $status = null ) { public function serve_request( $path = null ) { $this->send_header( 'Content-Type', 'application/json; charset=' . get_option( 'blog_charset' ), true ); + // Mitigate possible JSONP Flash attacks + // http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ + $this->send_header( 'X-Content-Type-Options', 'nosniff' ); + // Proper filter for turning off the JSON API. It is on by default. $enabled = apply_filters( 'json_enabled', true ); @@ -268,7 +272,9 @@ public function serve_request( $path = null ) { $result = json_encode( $this->prepare_response( $result ) ); if ( isset( $_GET['_jsonp'] ) ) { - echo $_GET['_jsonp'] . '(' . $result . ')'; + // Prepend '/**/' to mitigate possible JSONP Flash attacks + // http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ + echo '/**/' . $_GET['_jsonp'] . '(' . $result . ')'; } else { echo $result; } From e55c8a6bfbcbe34b7e575c03d4318fc21b80e1a6 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 16 Jul 2014 16:03:32 +1000 Subject: [PATCH 370/515] Add initial pass at splitting meta out --- lib/class-wp-json-meta-posts.php | 121 ++++++++++ lib/class-wp-json-meta.php | 388 ++++++++++++++++++++++++++++++ lib/class-wp-json-posts.php | 401 ------------------------------- plugin.php | 8 + 4 files changed, 517 insertions(+), 401 deletions(-) create mode 100644 lib/class-wp-json-meta-posts.php create mode 100644 lib/class-wp-json-meta.php diff --git a/lib/class-wp-json-meta-posts.php b/lib/class-wp-json-meta-posts.php new file mode 100644 index 0000000000..3b018394cd --- /dev/null +++ b/lib/class-wp-json-meta-posts.php @@ -0,0 +1,121 @@ +\d+)/meta). Must include ID selector. + */ + protected $base = '/posts/(?P\d+)/meta'; + + /** + * Associated object type + * + * @var string Type slug ("post" or "user") + */ + protected $type = 'post'; + + /** + * Check that the object can be accessed + * + * @param mixed $id Object ID + * @return boolean|WP_Error + */ + protected function check_object( $id ) { + $id = (int) $id; + + if ( empty( $id ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + $post = get_post( $id, ARRAY_A ); + + if ( empty( $post['ID'] ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + if ( ! $this->check_edit_permission( $post ) ) { + return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); + } + + return true; + } + + /** + * Add meta to a post + * + * Ensures that the correct location header is sent with the response. + * + * @param int $id Post ID + * @param array $data { + * @type string|null $key Meta key + * @type string|null $key Meta value + * } + * @return bool|WP_Error + */ + public function add_meta( $id, $data ) { + $response = parent::add_meta( $id, $data ); + if ( is_wp_error( $response ) ) { + return $response; + } + + $response = json_ensure_response( $response ); + $data = $response->get_data(); + + $response->header( 'Location', json_url( '/posts/' . $id . '/meta/' . $data->id ) ); + + return $response; + } + + /** + * Add post meta to post responses + * + * Adds meta to post responses for the 'edit' context. + * + * @param WP_Error|WP_JSON_Response|array $data Post response data (or response) + * @param array $post Post data + * @param string $context Context for the prepared post. + * @return WP_Error|WP_JSON_Response|array Filtered data + */ + public function add_post_meta_data( $data, $post, $context ) { + if ( $context !== 'edit' || is_wp_error( $data ) ) { + return $data; + } + + // Ensure we're working with a response, just to be sure + $response = json_ensure_response( $data ); + $data = $response->get_data(); + + // Permissions have already been checked at this point, so no need to + // check again + $data['post_meta'] = $this->get_all_meta( $post['ID'] ); + if ( is_wp_error( $data['post_meta'] ) ) { + return $data['post_meta']; + } + + $response->set_data( $data ); + return $response; + } + + /** + * Add post meta on post update + * + * Handles adding/updating post meta when creating or updating posts. + * + * @param array $post New post data + * @param array $data Raw submitted data + * @return array|WP_Error Post data on success, post meta error otherwise + */ + public function insert_post_meta( $post, $data ) { + // Post meta + if ( ! empty( $data['post_meta'] ) ) { + $result = $this->handle_post_meta_action( $post['ID'], $data['post_meta'] ); + + if ( is_wp_error( $result ) ) { + return $result; + } + } + + return $post; + } +} \ No newline at end of file diff --git a/lib/class-wp-json-meta.php b/lib/class-wp-json-meta.php new file mode 100644 index 0000000000..92076c9283 --- /dev/null +++ b/lib/class-wp-json-meta.php @@ -0,0 +1,388 @@ +base ) ) { + _doing_it_wrong( 'WP_JSON_Meta::__construct', __( 'The route base must be overridden' ), 'WPAPI-1.2' ); + return; + } + if ( empty( $this->type ) ) { + _doing_it_wrong( 'WP_JSON_Meta::__construct', __( 'The object type must be overridden' ), 'WPAPI-1.2' ); + return; + } + } + + /** + * Register the meta-related routes + * + * @param array $routes Existing routes + * @return array Modified routes + */ + public function register_routes( $routes ) { + $routes[ $this->base ] = array( + array( array( $this, 'get_all_meta' ), WP_JSON_Server::READABLE ), + array( array( $this, 'add_meta' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), + ); + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_meta' ), WP_JSON_Server::READABLE ), + array( array( $this, 'update_meta' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), + array( array( $this, 'delete_meta' ), WP_JSON_Server::DELETABLE ), + ); + + return $routes; + } + + abstract protected function check_object( $id ); + + protected function get_id_column() { + return ( 'user' === $this->type ) ? 'umeta_id' : 'meta_id'; + } + + protected function get_parent_column() { + return ( 'user' === $this->type ) ? 'user_id' : 'post_id'; + } + + /** + * Retrieve custom fields for object + * + * @param int $id Object ID + * @return (array[]|WP_Error) List of meta object data on success, WP_Error otherwise + */ + public function get_all_meta( $id ) { + $check = $this->check_object( $id ); + if ( is_wp_error( $check ) ) { + return $check; + } + + global $wpdb; + $table = _get_meta_table( $this->type ); + $id_column = $this->get_id_column(); + + $results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_key, meta_value FROM $table WHERE $id_column = %d", $id ) ); + + $meta = array(); + + foreach ( $results as $row ) { + $value = $this->prepare_meta( $id, $row, true ); + + if ( is_wp_error( $value ) ) { + continue; + } + + $meta[] = $value; + } + + return apply_filters( 'json_prepare_meta', $meta, $id ); + } + + /** + * Retrieve custom field object. + * + * @param int $id Object ID + * @param int $mid Metadata ID + * @return array|WP_Error Meta object data on success, WP_Error otherwise + */ + public function get_meta( $id, $mid ) { + $check = $this->check_object( $id ); + if ( is_wp_error( $check ) ) { + return $check; + } + + $parent_column = $this->get_parent_column(); + $meta = get_metadata_by_mid( $this->type, $mid ); + + if ( empty( $meta ) ) { + return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); + } + + if ( absint( $meta->$parent_column ) !== $id ) { + return new WP_Error( 'json_meta_' . $this->type . '_mismatch', __( 'Meta does not belong to this object' ), array( 'status' => 400 ) ); + } + + return $this->prepare_meta( $id, $meta ); + } + + /** + * Prepares meta data for return as an object + * + * @param int $parent_id Object ID + * @param stdClass $data Metadata row from database + * @param boolean $is_serialized Is the value field still serialized? (False indicates the value has been unserialized) + * @return array|WP_Error Meta object data on success, WP_Error otherwise + */ + protected function prepare_meta( $parent_id, $data, $is_raw = false ) { + $ID = $data->meta_id; + $key = $data->meta_key; + $value = $data->meta_value; + + // Don't expose protected fields. + if ( is_protected_meta( $key ) ) { + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $key ), array( 'status' => 403 ) ); + } + + // Normalize serialized strings + if ( $is_raw && is_serialized_string( $value ) ) { + $value = unserialize( $value ); + } + + // Don't expose serialized data + if ( is_serialized( $value ) || ! is_string( $value ) ) { + return new WP_Error( 'json_meta_protected', sprintf( __( '%s contains serialized data.'), $key ), array( 'status' => 403 ) ); + } + + $meta = array( + 'ID' => (int) $ID, + 'key' => $key, + 'value' => $value, + ); + + return apply_filters( 'json_prepare_meta_value', $meta, $parent_id ); + } + + /** + * Update/add/delete meta for an object + * + * Meta data is expected to be sent in the same format as it's output: + * + * { + * "ID": 42, + * "key" : "meta_key", + * "value" : "meta_value" + * } + * + * If ID is not specified, the meta value will be created; otherwise, the + * value (and key, if it differs) will be updated. If ID is specified, and + * the key is set to `null`, the data will be deleted. + * + * @param array $data + * @param int $parent_id + * @return bool|WP_Error + */ + public function handle_inline_meta( $parent_id, $data ) { + foreach ( $data as $meta_array ) { + if ( empty( $meta_array['ID'] ) ) { + // Creation + $result = $this->add_meta( $parent_id, $meta_array ); + } else { + // Update + $result = $this->update_meta( $parent_id, $meta_array['ID'], $meta_array ); + } + + if ( is_wp_error( $result ) ) { + return $result; + } + } + + return true; + } + + /** + * Add meta to an object + * + * @param int $id Object ID + * @param array $data { + * @type string|null $key Meta key + * @type string|null $key Meta value + * } + * @return bool|WP_Error + */ + public function update_meta( $id, $mid, $data ) { + $id = (int) $id; + $mid = (int) $mid; + + $check = $this->check_object( $id ); + if ( is_wp_error( $check ) ) { + return $check; + } + + $parent_column = $this->get_parent_column(); + $current = get_metadata_by_mid( $this->type, $mid ); + + if ( empty( $current ) ) { + return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); + } + + if ( absint( $current->$parent_column ) !== $id ) { + return new WP_Error( 'json_meta_' . $this->type . '_mismatch', __( 'Meta does not belong to this object' ), array( 'status' => 400 ) ); + } + + if ( ! array_key_exists( 'key', $data ) ) { + $data['key'] = $current->meta_key; + } + + if ( ! array_key_exists( 'value', $data ) ) { + $data['value'] = $current->meta_value; + } + + if ( empty( $data['key'] ) ) { + return new WP_Error( 'json_meta_invalid_key', __( 'Invalid meta key.' ), array( 'status' => 400 ) ); + } + + // for now let's not allow updating of arrays, objects or serialized values. + if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { + return new WP_Error( 'json_meta_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); + } + + if ( ! $this->is_valid_meta_data( $data['value'] ) ) { + return new WP_Error( 'json_meta_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); + } + + if ( is_protected_meta( $current->meta_key ) ) { + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $current->meta_key ), array( 'status' => 403 ) ); + } + + if ( is_protected_meta( $data['key'] ) ) { + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $data['key'] ), array( 'status' => 403 ) ); + } + + // update_metadata_by_mid will return false if these are equal, so check + // first and pass through + if ( $data['value'] === $current->meta_value && $data['key'] === $current->meta_key ) { + return $this->get_meta( $id, $mid ); + } + + $key = wp_slash( $data['key'] ); + $value = wp_slash( $data['value'] ); + + if ( ! update_metadata_by_mid( $this->type, $mid, $value, $key ) ) { + return new WP_Error( 'json_meta_could_not_update', __( 'Could not update meta.' ), array( 'status' => 500 ) ); + } + + return $this->get_meta( $id, $mid ); + } + + /** + * Check if the data provided is valid data + * + * Excludes serialized data from being sent via the API. + * + * @see https://github.com/WP-API/WP-API/pull/68 + * @param mixed $data Data to be checked + * @return boolean Whether the data is valid or not + */ + protected function is_valid_meta_data( $data ) { + if ( is_array( $data ) || is_object( $data ) || is_serialized( $data ) ) { + return false; + } + + return true; + } + + /** + * Add meta to an object + * + * @param int $id Object ID + * @param array $data { + * @type string|null $key Meta key + * @type string|null $key Meta value + * } + * @return bool|WP_Error + */ + public function add_meta( $id, $data ) { + $check = $this->check_object( $id ); + if ( is_wp_error( $check ) ) { + return $check; + } + + if ( ! array_key_exists( 'key', $data ) ) { + return new WP_Error( 'json_meta_missing_key', __( 'Missing meta key.' ), array( 'status' => 400 ) ); + } + if ( ! array_key_exists( 'value', $data ) ) { + return new WP_Error( 'json_meta_missing_value', __( 'Missing meta value.' ), array( 'status' => 400 ) ); + } + + if ( empty( $data['key'] ) ) { + return new WP_Error( 'json_meta_invalid_key', __( 'Invalid meta key.' ), array( 'status' => 400 ) ); + } + + if ( ! $this->is_valid_meta_data( $data['value'] ) ) { + // for now let's not allow updating of arrays, objects or serialized values. + return new WP_Error( 'json_meta_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); + } + + if ( is_protected_meta( $data['key'] ) ) { + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $data['key'] ), array( 'status' => 403 ) ); + } + + $meta_key = wp_slash( $data['key'] ); + $value = wp_slash( $data['value'] ); + + $result = add_metadata( $this->type, $id, $meta_key, $value ); + + if ( ! $result ) { + return new WP_Error( 'json_meta_could_not_add', __( 'Could not add meta.' ), array( 'status' => 400 ) ); + } + + $response = json_ensure_response( $this->get_meta( $id, $result ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $response->set_status( 201 ); + return $response; + } + + /** + * Delete meta from an object + * + * @param int $id Object ID + * @param int $mid Metadata ID + * @return array|WP_Error Message on success, WP_Error otherwise + */ + public function delete_meta( $id, $mid ) { + $check = $this->check_object( $id ); + if ( is_wp_error( $check ) ) { + return $check; + } + + $parent_column = $this->get_parent_column(); + $current = get_metadata_by_mid( $this->type, $mid ); + + if ( empty( $current ) ) { + return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); + } + + if ( absint( $current->$parent_column ) !== $id ) { + return new WP_Error( 'json_meta_' . $this->type . '_mismatch', __( 'Meta does not belong to this object' ), array( 'status' => 400 ) ); + } + + // for now let's not allow updating of arrays, objects or serialized values. + if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { + return new WP_Error( 'json_meta_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); + } + + if ( is_protected_meta( $current->meta_key ) ) { + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $current->meta_key ), array( 'status' => 403 ) ); + } + + if ( ! delete_metadata_by_mid( $this->type, $mid ) ) { + return new WP_Error( 'json_meta_could_not_delete', __( 'Could not delete meta.' ), array( 'status' => 500 ) ); + } + + return array( 'message' => __( 'Deleted meta' ) );; + } +} diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 27ce5e69a6..c842f280c4 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -40,17 +40,6 @@ public function register_routes( $routes ) { array( $this, 'get_revisions' ), WP_JSON_Server::READABLE ), - // Meta - '/posts/(?P\d+)/meta' => array( - array( array( $this, 'get_all_meta' ), WP_JSON_Server::READABLE ), - array( array( $this, 'add_meta' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), - ), - '/posts/(?P\d+)/meta/(?P\d+)' => array( - array( array( $this, 'get_meta' ), WP_JSON_Server::READABLE ), - array( array( $this, 'update_meta' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), - array( array( $this, 'delete_meta' ), WP_JSON_Server::DELETABLE ), - ), - // Comments '/posts/(?P\d+)/comments' => array( array( array( $this, 'get_comments' ), WP_JSON_Server::READABLE ), @@ -722,7 +711,6 @@ protected function prepare_post( $post, $context = 'view' ) { 'content_raw' => $post['post_content'], 'excerpt_raw' => $post['post_excerpt'], 'guid_raw' => $post['guid'], - 'post_meta' => $this->get_all_meta( $post['ID'] ), ); // Dates @@ -788,14 +776,6 @@ protected function prepare_post( $post, $context = 'view' ) { if ( 'edit' === $context ) { if ( current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { - if ( is_wp_error( $post_fields_raw['post_meta'] ) ) { - $GLOBALS['post'] = $previous_post; - if ( $previous_post ) { - setup_postdata( $previous_post ); - } - return $post_fields_raw['post_meta']; - } - $_post = array_merge( $_post, $post_fields_raw ); } else { $GLOBALS['post'] = $previous_post; @@ -860,378 +840,6 @@ protected function prepare_excerpt( $excerpt ) { return $excerpt; } - /** - * Retrieve custom fields for post. - * - * @param int $id Post ID - * @return (array[]|WP_Error) List of meta object data on success, WP_Error otherwise - */ - public function get_all_meta( $id ) { - $id = (int) $id; - - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - - $post = get_post( $id, ARRAY_A ); - - if ( empty( $post['ID'] ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - - if ( ! $this->check_edit_permission( $post ) ) { - return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); - } - - global $wpdb; - $table = _get_meta_table( 'post' ); - $results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_key, meta_value FROM $table WHERE post_id = %d", $id ) ); - - $meta = array(); - - foreach ( $results as $row ) { - $value = $this->prepare_meta( $id, $row, true ); - - if ( is_wp_error( $value ) ) { - continue; - } - - $meta[] = $value; - } - - return apply_filters( 'json_prepare_meta', $meta, $id ); - } - - /** - * Retrieve custom field object. - * - * @param int $id Post ID - * @param int $mid Metadata ID - * @return array|WP_Error Meta object data on success, WP_Error otherwise - */ - public function get_meta( $id, $mid ) { - $id = (int) $id; - - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - - $post = get_post( $id, ARRAY_A ); - - if ( empty( $post['ID'] ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - - if ( ! $this->check_edit_permission( $post ) ) { - return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); - } - - $meta = get_metadata_by_mid( 'post', $mid ); - - if ( empty( $meta ) ) { - return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); - } - - if ( absint( $meta->post_id ) !== $id ) { - return new WP_Error( 'json_meta_post_mismatch', __( 'Meta does not belong to this post' ), array( 'status' => 400 ) ); - } - - return $this->prepare_meta( $id, $meta ); - } - - /** - * Prepares meta data for return as an object - * - * @param int $post Post ID - * @param stdClass $data Metadata row from database - * @param boolean $is_serialized Is the value field still serialized? (False indicates the value has been unserialized) - * @return array|WP_Error Meta object data on success, WP_Error otherwise - */ - protected function prepare_meta( $post, $data, $is_raw = false ) { - $ID = $data->meta_id; - $key = $data->meta_key; - $value = $data->meta_value; - - // Don't expose protected fields. - if ( is_protected_meta( $key ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $key ), array( 'status' => 403 ) ); - } - - // Normalize serialized strings - if ( $is_raw && is_serialized_string( $value ) ) { - $value = unserialize( $value ); - } - - // Don't expose serialized data - if ( is_serialized( $value ) || ! is_string( $value ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s contains serialized data.'), $key ), array( 'status' => 403 ) ); - } - - $meta = array( - 'ID' => (int) $ID, - 'key' => $key, - 'value' => $value, - ); - - return apply_filters( 'json_prepare_meta_value', $meta, $post ); - } - - /** - * Update/add/delete post meta for a post. Post meta is expected to be sent like so: - * { - * post_meta : [ - * { - * "ID": 42, - * "key" : "meta_key", - * "value" : "meta_value" - * } - * ] - * } - * - * If ID is not specified, the meta value will be created; otherwise, the - * value (and key, if it differs) will be updated. If ID is specified, and - * the key is set to `null`, the data will be deleted. - * - * @param array $data - * @param int $post_id - * @return bool|WP_Error - */ - protected function handle_post_meta_action( $post_id, $data ) { - foreach ( $data as $meta_array ) { - if ( empty( $meta_array['ID'] ) ) { - // Creation - $result = $this->add_meta( $post_id, $meta_array ); - } else { - // Update - $result = $this->update_meta( $post_id, $meta_array['ID'], $meta_array ); - } - - if ( is_wp_error( $result ) ) { - return $result; - } - } - - return true; - } - - /** - * Add meta to a post - * - * @param int $id Post ID - * @param array $data { - * @type string|null $key Meta key - * @type string|null $key Meta value - * } - * @return bool|WP_Error - */ - public function update_meta( $id, $mid, $data ) { - $id = (int) $id; - $mid = (int) $mid; - - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - - $post = get_post( $id, ARRAY_A ); - - if ( empty( $post['ID'] ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - - if ( ! $this->check_edit_permission( $post ) ) { - return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); - } - - $current = get_metadata_by_mid( 'post', $mid ); - - if ( empty( $current ) ) { - return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); - } - - if ( absint( $current->post_id ) !== $id ) { - return new WP_Error( 'json_meta_post_mismatch', __( 'Meta does not belong to this post' ), array( 'status' => 400 ) ); - } - - if ( ! array_key_exists( 'key', $data ) ) { - $data['key'] = $current->meta_key; - } - - if ( ! array_key_exists( 'value', $data ) ) { - $data['value'] = $current->meta_value; - } - - if ( empty( $data['key'] ) ) { - return new WP_Error( 'json_meta_invalid_key', __( 'Invalid meta key.' ), array( 'status' => 400 ) ); - } - - // for now let's not allow updating of arrays, objects or serialized values. - if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { - return new WP_Error( 'json_post_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); - } - - if ( ! $this->is_valid_meta_data( $data['value'] ) ) { - return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); - } - - if ( is_protected_meta( $current->meta_key ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $current->meta_key ), array( 'status' => 403 ) ); - } - - if ( is_protected_meta( $data['key'] ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $data['key'] ), array( 'status' => 403 ) ); - } - - // update_metadata_by_mid will return false if these are equal, so check - // first and pass through - if ( $data['value'] === $current->meta_value && $data['key'] === $current->meta_key ) { - return $this->get_meta( $id, $mid ); - } - - $key = wp_slash( $data['key'] ); - $value = wp_slash( $data['value'] ); - - if ( ! update_metadata_by_mid( 'post', $mid, $value, $key ) ) { - return new WP_Error( 'json_meta_could_not_update', __( 'Could not update post meta.' ), array( 'status' => 500 ) ); - } - - return $this->get_meta( $id, $mid ); - } - - /** - * Check if the data provided is valid data - * - * Excludes serialized data from being sent via the API. - * - * @see https://github.com/WP-API/WP-API/pull/68 - * @param mixed $data Data to be checked - * @return boolean Whether the data is valid or not - */ - protected function is_valid_meta_data( $data ) { - if ( is_array( $data ) || is_object( $data ) || is_serialized( $data ) ) { - return false; - } - - return true; - } - - /** - * Add meta to a post - * - * @param int $id Post ID - * @param array $data { - * @type string|null $key Meta key - * @type string|null $key Meta value - * } - * @return bool|WP_Error - */ - public function add_meta( $id, $data ) { - $id = (int) $id; - - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - - $post = get_post( $id, ARRAY_A ); - - if ( empty( $post['ID'] ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - - if ( ! $this->check_edit_permission( $post ) ) { - return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); - } - - if ( ! array_key_exists( 'key', $data ) ) { - return new WP_Error( 'json_post_missing_key', __( 'Missing meta key.' ), array( 'status' => 400 ) ); - } - if ( ! array_key_exists( 'value', $data ) ) { - return new WP_Error( 'json_post_missing_value', __( 'Missing meta value.' ), array( 'status' => 400 ) ); - } - - if ( empty( $data['key'] ) ) { - return new WP_Error( 'json_meta_invalid_key', __( 'Invalid meta key.' ), array( 'status' => 400 ) ); - } - - if ( ! $this->is_valid_meta_data( $data['value'] ) ) { - // for now let's not allow updating of arrays, objects or serialized values. - return new WP_Error( 'json_post_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); - } - - if ( is_protected_meta( $data['key'] ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $data['key'] ), array( 'status' => 403 ) ); - } - - $meta_key = wp_slash( $data['key'] ); - $value = wp_slash( $data['value'] ); - - $result = add_post_meta( $id, $meta_key, $value ); - - if ( ! $result ) { - return new WP_Error( 'json_meta_could_not_add', __( 'Could not add post meta.' ), array( 'status' => 400 ) ); - } - - $response = json_ensure_response( $this->get_meta( $id, $result ) ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - $response->set_status( 201 ); - $response->header( 'Location', json_url( '/posts/' . $id . '/meta/' . $result ) ); - - return $response; - } - - /** - * Delete meta from a post - * - * @param int $id Post ID - * @param int $mid Metadata ID - * @return array|WP_Error Message on success, WP_Error otherwise - */ - public function delete_meta( $id, $mid ) { - $id = (int) $id; - - if ( empty( $id ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - - $post = get_post( $id, ARRAY_A ); - - if ( empty( $post['ID'] ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - - if ( ! $this->check_edit_permission( $post ) ) { - return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); - } - - $current = get_metadata_by_mid( 'post', $mid ); - - if ( empty( $current ) ) { - return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); - } - - if ( absint( $current->post_id ) !== $id ) { - return new WP_Error( 'json_meta_post_mismatch', __( 'Meta does not belong to this post' ), array( 'status' => 400 ) ); - } - - // for now let's not allow updating of arrays, objects or serialized values. - if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { - return new WP_Error( 'json_post_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); - } - - if ( is_protected_meta( $current->meta_key ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $current->meta_key ), array( 'status' => 403 ) ); - } - - if ( ! delete_metadata_by_mid( 'post', $mid ) ) { - return new WP_Error( 'json_meta_could_not_add', __( 'Could not delete post meta.' ), array( 'status' => 500 ) ); - } - - return array( 'message' => __( 'Deleted meta' ) );; - } - /** * Embed post type data into taxonomy data * @@ -1479,15 +1087,6 @@ protected function insert_post( $data ) { $post['ID'] = $post_ID; } - // Post meta - if ( ! empty( $data['post_meta'] ) ) { - $result = $this->handle_post_meta_action( $post_ID, $data['post_meta'] ); - - if ( is_wp_error( $result ) ) { - return $result; - } - } - // Sticky if ( isset( $data['sticky'] ) ) { if ( $data['sticky'] ) { diff --git a/plugin.php b/plugin.php index 41c1875852..e7aefe5461 100644 --- a/plugin.php +++ b/plugin.php @@ -33,6 +33,8 @@ include_once( dirname( __FILE__ ) . '/lib/class-wp-json-pages.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-media.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-taxonomies.php' ); +include_once( dirname( __FILE__ ) . '/lib/class-wp-json-meta.php' ); +include_once( dirname( __FILE__ ) . '/lib/class-wp-json-meta-posts.php' ); /** * Register our rewrite rules for the API @@ -87,6 +89,12 @@ function json_api_default_filters( $server ) { $wp_json_pages = new WP_JSON_Pages( $server ); $wp_json_pages->register_filters(); + // Post meta + $wp_json_post_meta = new WP_JSON_Meta_Posts( $server ); + add_filter( 'json_endpoints', array( $wp_json_post_meta, 'register_routes' ), 0 ); + add_filter( 'json_prepare_post', array( $wp_json_post_meta, 'add_post_meta_data' ), 10, 3 ); + add_filter( 'json_insert_post', array( $wp_json_post_meta, 'insert_post_meta' ), 10, 2 ); + // Media $wp_json_media = new WP_JSON_Media( $server ); add_filter( 'json_endpoints', array( $wp_json_media, 'register_routes' ), 1 ); From 8b7aa298a9144483d7de32726ff3aa7c005c825e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 16 Jul 2014 16:17:25 +1000 Subject: [PATCH 371/515] Add 1.1.1 changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2699b0ed0..a320ee40b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 1.1.1 + +- Mitigate Flash CSRF exploit + + Using the API's JSONP support, it's possible to control the first bytes of the + response sent to the browser. Combining this with an ASCII-encoded SWF allows + arbitrary SWFs to be served from the site, allowing bypassing the same-origin + policy built in to browsers. + + While the API includes CSRF protection and is not directly vulnerable, this + can be used to bypass other browser origin controls. + + Reported by @iandunn on 2014-07-10. + + (props @iandunn, @rmccue, [#356][gh-356]) + +[View all changes](https://github.com/rmccue/WP-API/compare/1.0...1.1) + +[gh-356]: https://github.com/WP-API/WP-API/issues/356 + ## 1.1 - Add new routes for taxonomies and terms. From be4bb5c4e138615414783136385b65db7ad126f2 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 16 Jul 2014 16:03:42 +1000 Subject: [PATCH 372/515] Add backwards compatibility shims for meta methods --- lib/class-wp-json-meta-posts.php | 14 ++++ lib/class-wp-json-posts.php | 135 +++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/lib/class-wp-json-meta-posts.php b/lib/class-wp-json-meta-posts.php index 3b018394cd..d4c1054a2d 100644 --- a/lib/class-wp-json-meta-posts.php +++ b/lib/class-wp-json-meta-posts.php @@ -118,4 +118,18 @@ public function insert_post_meta( $post, $data ) { return $post; } + + /** + * Call protected method from WP_JSON_Posts + * + * WPAPI-1.2 deprecated a bunch of protected methods by moving them to this + * class. This proxy method is added to call those methods. + * + * @param string $method Method name + * @param array $args Method arguments + * @return mixed Return value from the method + */ + public function _deprecated_call( $method, $args ) { + return call_user_func_array( array( $this, $method ), $args ); + } } \ No newline at end of file diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index c842f280c4..ee48304d06 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1216,4 +1216,139 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen return apply_filters( 'json_prepare_comment', $data, $comment, $context ); } + + /** + * Retrieve custom fields for object + * + * @deprecated WPAPI-1.2 + * + * @param int $id Object ID + * @return (array[]|WP_Error) List of meta object data on success, WP_Error otherwise + */ + public function get_all_meta( $id ) { + _deprecated_function( 'WP_JSON_Posts::get_all_meta', 'WPAPI-1.2', 'WP_JSON_Meta_Posts::get_all_meta' ); + + $handler = new WP_JSON_Meta_Posts( $this->server ); + return $handler->get_all_meta( $id ); + } + + /** + * Add meta to a post + * + * Ensures that the correct location header is sent with the response. + * + * @deprecated WPAPI-1.2 + * + * @param int $id Post ID + * @param array $data { + * @type string|null $key Meta key + * @type string|null $key Meta value + * } + * @return bool|WP_Error + */ + public function add_meta( $id, $data ) { + _deprecated_function( 'WP_JSON_Posts::add_meta', 'WPAPI-1.2', 'WP_JSON_Meta_Posts::add_meta' ); + + $handler = new WP_JSON_Meta_Posts( $this->server ); + return $handler->add_meta( $id, $data ); + } + + /** + * Retrieve custom field object. + * + * @deprecated WPAPI-1.2 + * + * @param int $id Object ID + * @param int $mid Metadata ID + * @return array|WP_Error Meta object data on success, WP_Error otherwise + */ + public function get_meta( $id, $mid ) { + _deprecated_function( 'WP_JSON_Posts::get_meta', 'WPAPI-1.2', 'WP_JSON_Meta_Posts::get_meta' ); + + $handler = new WP_JSON_Meta_Posts( $this->server ); + return $handler->get_meta( $id, $mid ); + } + + /** + * Add meta to an object + * + * @deprecated WPAPI-1.2 + * + * @param int $id Object ID + * @param array $data { + * @type string|null $key Meta key + * @type string|null $key Meta value + * } + * @return bool|WP_Error + */ + public function update_meta( $id, $mid, $data ) { + _deprecated_function( 'WP_JSON_Posts::update_meta', 'WPAPI-1.2', 'WP_JSON_Meta_Posts::update_meta' ); + + $handler = new WP_JSON_Meta_Posts( $this->server ); + return $handler->update_meta( $id, $mid, $data ); + } + + /** + * Delete meta from an object + * + * @deprecated WPAPI-1.2 + * + * @param int $id Object ID + * @param int $mid Metadata ID + * @return array|WP_Error Message on success, WP_Error otherwise + */ + public function delete_meta( $id, $mid ) { + _deprecated_function( 'WP_JSON_Posts::delete_meta', 'WPAPI-1.2', 'WP_JSON_Meta_Posts::delete_meta' ); + + $handler = new WP_JSON_Meta_Posts( $this->server ); + return $handler->delete_meta( $id, $mid, $data ); + } + + /** + * Prepares meta data for return as an object + * + * @deprecated WPAPI-1.2 + * + * @param int $post Object ID + * @param stdClass $data Metadata row from database + * @param boolean $is_raw Is the value field still serialized? (False indicates the value has been unserialized) + * @return array|WP_Error Meta object data on success, WP_Error otherwise + */ + protected function prepare_meta( $post, $data, $is_raw = false ) { + _deprecated_function( 'WP_JSON_Posts::prepare_meta', 'WPAPI-1.2', 'WP_JSON_Meta_Posts::prepare_meta' ); + + $handler = new WP_JSON_Meta_Posts( $this->server ); + return $handler->_deprecated_call( 'prepare_meta', array( $post, $data, $is_raw ) ); + } + + /** + * Update/add/delete meta for an object + * + * @deprecated WPAPI-1.2 + * + * @param array $data + * @param int $parent_id + * @return bool|WP_Error + */ + protected function handle_post_meta_action( $post_id, $data ) { + _deprecated_function( 'WP_JSON_Posts::handle_post_meta_action', 'WPAPI-1.2', 'WP_JSON_Meta_Posts::handle_inline_meta' ); + + $handler = new WP_JSON_Meta_Posts( $this->server ); + return $handler->_deprecated_call( 'handle_inline_meta', array( $post, $data, $is_raw ) ); + } + + /** + * Check if the data provided is valid data + * + * @deprecated WPAPI-1.2 + * + * @param mixed $data Data to be checked + * @return boolean Whether the data is valid or not + */ + protected function is_valid_meta_data( $data ) { + _deprecated_function( 'WP_JSON_Posts::is_valid_meta_data', 'WPAPI-1.2', 'WP_JSON_Meta_Posts::is_valid_meta_data' ); + + $handler = new WP_JSON_Meta_Posts( $this->server ); + return $handler->_deprecated_call( 'is_valid_meta_data', array( $post, $data, $is_raw ) ); + } } From f3a8c1c5cccfe40550e448177f95782ab7ebe125 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 17 Jul 2014 15:28:51 +1000 Subject: [PATCH 373/515] Add missing permissions method --- lib/class-wp-json-meta-posts.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/class-wp-json-meta-posts.php b/lib/class-wp-json-meta-posts.php index d4c1054a2d..9c8ee718c9 100644 --- a/lib/class-wp-json-meta-posts.php +++ b/lib/class-wp-json-meta-posts.php @@ -15,6 +15,22 @@ class WP_JSON_Meta_Posts extends WP_JSON_Meta { */ protected $type = 'post'; + /** + * Check if we can edit a post + * + * @param array $post Post data + * @return boolean Can we edit it? + */ + protected function check_edit_permission( $post ) { + $post_type = get_post_type_object( $post['post_type'] ); + + if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + return false; + } + + return true; + } + /** * Check that the object can be accessed * From 2a093b3b5fb047a8bbd22e9a23cf4fecb771fa52 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 17 Jul 2014 15:29:07 +1000 Subject: [PATCH 374/515] Use correct variables --- lib/class-wp-json-meta-posts.php | 4 ++-- lib/class-wp-json-meta.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-json-meta-posts.php b/lib/class-wp-json-meta-posts.php index 9c8ee718c9..ee2b942904 100644 --- a/lib/class-wp-json-meta-posts.php +++ b/lib/class-wp-json-meta-posts.php @@ -76,9 +76,9 @@ public function add_meta( $id, $data ) { } $response = json_ensure_response( $response ); - $data = $response->get_data(); + $data = (object) $response->get_data(); - $response->header( 'Location', json_url( '/posts/' . $id . '/meta/' . $data->id ) ); + $response->header( 'Location', json_url( '/posts/' . $id . '/meta/' . $data->ID ) ); return $response; } diff --git a/lib/class-wp-json-meta.php b/lib/class-wp-json-meta.php index 92076c9283..52cbdab7ba 100644 --- a/lib/class-wp-json-meta.php +++ b/lib/class-wp-json-meta.php @@ -79,9 +79,9 @@ public function get_all_meta( $id ) { global $wpdb; $table = _get_meta_table( $this->type ); - $id_column = $this->get_id_column(); + $parent_column = $this->get_parent_column(); - $results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_key, meta_value FROM $table WHERE $id_column = %d", $id ) ); + $results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_key, meta_value FROM $table WHERE $parent_column = %d", $id ) ); $meta = array(); From 3193a5bde6a2f984315d93bc762bf421c43132ef Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 17 Jul 2014 15:29:22 +1000 Subject: [PATCH 375/515] Use old error codes for backwards compatibility --- lib/class-wp-json-meta.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/class-wp-json-meta.php b/lib/class-wp-json-meta.php index 52cbdab7ba..2c4a641ddc 100644 --- a/lib/class-wp-json-meta.php +++ b/lib/class-wp-json-meta.php @@ -243,11 +243,13 @@ public function update_meta( $id, $mid, $data ) { // for now let's not allow updating of arrays, objects or serialized values. if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { - return new WP_Error( 'json_meta_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); + $code = ( $this->type === 'post' ) ? 'json_post_invalid_action' : 'json_meta_invalid_action'; + return new WP_Error( $code, __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); } if ( ! $this->is_valid_meta_data( $data['value'] ) ) { - return new WP_Error( 'json_meta_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); + $code = ( $this->type === 'post' ) ? 'json_post_invalid_action' : 'json_meta_invalid_action'; + return new WP_Error( $code, __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } if ( is_protected_meta( $current->meta_key ) ) { @@ -308,10 +310,12 @@ public function add_meta( $id, $data ) { } if ( ! array_key_exists( 'key', $data ) ) { - return new WP_Error( 'json_meta_missing_key', __( 'Missing meta key.' ), array( 'status' => 400 ) ); + $code = ( $this->type === 'post' ) ? 'json_post_missing_key' : 'json_meta_missing_key'; + return new WP_Error( $code, __( 'Missing meta key.' ), array( 'status' => 400 ) ); } if ( ! array_key_exists( 'value', $data ) ) { - return new WP_Error( 'json_meta_missing_value', __( 'Missing meta value.' ), array( 'status' => 400 ) ); + $code = ( $this->type === 'post' ) ? 'json_post_missing_value' : 'json_meta_missing_value'; + return new WP_Error( $code, __( 'Missing meta value.' ), array( 'status' => 400 ) ); } if ( empty( $data['key'] ) ) { @@ -319,8 +323,10 @@ public function add_meta( $id, $data ) { } if ( ! $this->is_valid_meta_data( $data['value'] ) ) { + $code = ( $this->type === 'post' ) ? 'json_post_invalid_action' : 'json_meta_invalid_action'; + // for now let's not allow updating of arrays, objects or serialized values. - return new WP_Error( 'json_meta_invalid_action', __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); + return new WP_Error( $code, __( 'Invalid provided meta data for action.' ), array( 'status' => 400 ) ); } if ( is_protected_meta( $data['key'] ) ) { @@ -372,7 +378,8 @@ public function delete_meta( $id, $mid ) { // for now let's not allow updating of arrays, objects or serialized values. if ( ! $this->is_valid_meta_data( $current->meta_value ) ) { - return new WP_Error( 'json_meta_invalid_action', __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); + $code = ( $this->type === 'post' ) ? 'json_post_invalid_action' : 'json_meta_invalid_action'; + return new WP_Error( $code, __( 'Invalid existing meta data for action.' ), array( 'status' => 400 ) ); } if ( is_protected_meta( $current->meta_key ) ) { From 888015d46f15943c95f54da5a4dd9a9fdace7b2e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 17 Jul 2014 15:29:46 +1000 Subject: [PATCH 376/515] Switch tests to new meta class --- tests/test_json_posts_meta.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_json_posts_meta.php b/tests/test_json_posts_meta.php index 36c979f485..3b06f9f3d3 100644 --- a/tests/test_json_posts_meta.php +++ b/tests/test_json_posts_meta.php @@ -16,7 +16,7 @@ public function setUp() { $this->user_obj->add_role('author'); $this->fake_server = $this->getMock('WP_JSON_Server'); - $this->endpoint = new WP_JSON_Posts( $this->fake_server ); + $this->endpoint = new WP_JSON_Meta_Posts( $this->fake_server ); } public function test_get_meta() { @@ -306,7 +306,7 @@ public function test_add_meta_serialized_string() { } public function test_add_meta_failed_get() { - $this->endpoint = $this->getMock('WP_JSON_Posts', array('get_meta'), array( $this->fake_server ) ); + $this->endpoint = $this->getMock('WP_JSON_Meta_Posts', array('get_meta'), array( $this->fake_server ) ); $test_error = new WP_Error( 'json_test_error', 'Test error' ); $this->endpoint->expects($this->any())->method('get_meta')->will($this->returnValue($test_error)); From 068f0dc94e512016b922ca704cb34169ff3464c4 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 17 Jul 2014 16:17:54 +1000 Subject: [PATCH 377/515] Correct inline docs --- lib/class-wp-json-meta.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-meta.php b/lib/class-wp-json-meta.php index 2c4a641ddc..e962228d41 100644 --- a/lib/class-wp-json-meta.php +++ b/lib/class-wp-json-meta.php @@ -130,7 +130,7 @@ public function get_meta( $id, $mid ) { * * @param int $parent_id Object ID * @param stdClass $data Metadata row from database - * @param boolean $is_serialized Is the value field still serialized? (False indicates the value has been unserialized) + * @param boolean $is_raw Is the value field still serialized? (False indicates the value has been unserialized) * @return array|WP_Error Meta object data on success, WP_Error otherwise */ protected function prepare_meta( $parent_id, $data, $is_raw = false ) { From e8848eb9742f48d4670dd3fe874e2a290bb8a977 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 17 Jul 2014 16:19:31 +1000 Subject: [PATCH 378/515] json_prepare_post never sends a WP_JSON_Response The filter is used in the prepare_post method, which only returns data. Any extra headers/etc need to be added in a higher level. --- lib/class-wp-json-meta-posts.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/class-wp-json-meta-posts.php b/lib/class-wp-json-meta-posts.php index ee2b942904..f90b0ad06a 100644 --- a/lib/class-wp-json-meta-posts.php +++ b/lib/class-wp-json-meta-posts.php @@ -88,20 +88,16 @@ public function add_meta( $id, $data ) { * * Adds meta to post responses for the 'edit' context. * - * @param WP_Error|WP_JSON_Response|array $data Post response data (or response) + * @param WP_Error|array $data Post response data (or error) * @param array $post Post data * @param string $context Context for the prepared post. - * @return WP_Error|WP_JSON_Response|array Filtered data + * @return WP_Error|array Filtered data */ public function add_post_meta_data( $data, $post, $context ) { if ( $context !== 'edit' || is_wp_error( $data ) ) { return $data; } - // Ensure we're working with a response, just to be sure - $response = json_ensure_response( $data ); - $data = $response->get_data(); - // Permissions have already been checked at this point, so no need to // check again $data['post_meta'] = $this->get_all_meta( $post['ID'] ); @@ -109,8 +105,7 @@ public function add_post_meta_data( $data, $post, $context ) { return $data['post_meta']; } - $response->set_data( $data ); - return $response; + return $data; } /** From 9917cd10a1fe9fad46cdd3725cced47d87d63395 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 17 Jul 2014 16:24:41 +1000 Subject: [PATCH 379/515] Rename tests to match class file naming --- phpunit.xml.dist | 2 +- tests/bootstrap.php | 2 +- tests/{test_json_plugin.php => test-json-plugin.php} | 0 ...est_json_post_revisions.php => test-json-post-revisions.php} | 0 tests/{test_json_posts_meta.php => test-json-posts-meta.php} | 0 tests/{test_json_posts.php => test-json-posts.php} | 0 tests/{test_json_server.php => test-json-server.php} | 0 tests/{test_json_taxonomies.php => test-json-taxonomies.php} | 0 tests/{test_json_testcase.php => test-json-testcase.php} | 0 tests/{test_json_users.php => test-json-users.php} | 0 10 files changed, 2 insertions(+), 2 deletions(-) rename tests/{test_json_plugin.php => test-json-plugin.php} (100%) rename tests/{test_json_post_revisions.php => test-json-post-revisions.php} (100%) rename tests/{test_json_posts_meta.php => test-json-posts-meta.php} (100%) rename tests/{test_json_posts.php => test-json-posts.php} (100%) rename tests/{test_json_server.php => test-json-server.php} (100%) rename tests/{test_json_taxonomies.php => test-json-taxonomies.php} (100%) rename tests/{test_json_testcase.php => test-json-testcase.php} (100%) rename tests/{test_json_users.php => test-json-users.php} (100%) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 73f87977e1..e000b76677 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -9,7 +9,7 @@ - tests + tests diff --git a/tests/bootstrap.php b/tests/bootstrap.php index c17608ea0e..1b33b6c629 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -22,4 +22,4 @@ } // Helper classes -require_once dirname( __FILE__ ) . '/test_json_testcase.php'; +require_once dirname( __FILE__ ) . '/test-json-testcase.php'; diff --git a/tests/test_json_plugin.php b/tests/test-json-plugin.php similarity index 100% rename from tests/test_json_plugin.php rename to tests/test-json-plugin.php diff --git a/tests/test_json_post_revisions.php b/tests/test-json-post-revisions.php similarity index 100% rename from tests/test_json_post_revisions.php rename to tests/test-json-post-revisions.php diff --git a/tests/test_json_posts_meta.php b/tests/test-json-posts-meta.php similarity index 100% rename from tests/test_json_posts_meta.php rename to tests/test-json-posts-meta.php diff --git a/tests/test_json_posts.php b/tests/test-json-posts.php similarity index 100% rename from tests/test_json_posts.php rename to tests/test-json-posts.php diff --git a/tests/test_json_server.php b/tests/test-json-server.php similarity index 100% rename from tests/test_json_server.php rename to tests/test-json-server.php diff --git a/tests/test_json_taxonomies.php b/tests/test-json-taxonomies.php similarity index 100% rename from tests/test_json_taxonomies.php rename to tests/test-json-taxonomies.php diff --git a/tests/test_json_testcase.php b/tests/test-json-testcase.php similarity index 100% rename from tests/test_json_testcase.php rename to tests/test-json-testcase.php diff --git a/tests/test_json_users.php b/tests/test-json-users.php similarity index 100% rename from tests/test_json_users.php rename to tests/test-json-users.php From 9faa1fa982915b24347d577c3beee7a0d30a9582 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 17 Jul 2014 16:44:01 +1000 Subject: [PATCH 380/515] Add inline documentation for meta methods --- lib/class-wp-json-meta.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/class-wp-json-meta.php b/lib/class-wp-json-meta.php index e962228d41..8f90b81823 100644 --- a/lib/class-wp-json-meta.php +++ b/lib/class-wp-json-meta.php @@ -55,12 +55,28 @@ public function register_routes( $routes ) { return $routes; } + /** + * Check that the object is valid and can be accessed. + * + * @param mixed $id Object ID (can be any data from the API, will be validated) + * @return boolean|WP_Error True if valid and accessible, error otherwise. + */ abstract protected function check_object( $id ); + /** + * Get the meta ID column for the relevant table. + * + * @return string + */ protected function get_id_column() { return ( 'user' === $this->type ) ? 'umeta_id' : 'meta_id'; } + /** + * Get the object (parent) ID column for the relevant table. + * + * @return string + */ protected function get_parent_column() { return ( 'user' === $this->type ) ? 'user_id' : 'post_id'; } From 3b34a49c2a02bf7c583783444cb012385febb608 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 17 Jul 2014 16:45:05 +1000 Subject: [PATCH 381/515] Update inline docs to match standards --- lib/class-wp-json-meta-posts.php | 16 ++++++++-------- lib/class-wp-json-meta.php | 26 +++++++++++++------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/class-wp-json-meta-posts.php b/lib/class-wp-json-meta-posts.php index f90b0ad06a..e948f80ce3 100644 --- a/lib/class-wp-json-meta-posts.php +++ b/lib/class-wp-json-meta-posts.php @@ -2,21 +2,21 @@ class WP_JSON_Meta_Posts extends WP_JSON_Meta { /** - * Base route name + * Base route name. * * @var string Route base (e.g. /my-plugin/my-type/(?P\d+)/meta). Must include ID selector. */ protected $base = '/posts/(?P\d+)/meta'; /** - * Associated object type + * Associated object type. * * @var string Type slug ("post" or "user") */ protected $type = 'post'; /** - * Check if we can edit a post + * Check if we can edit a post. * * @param array $post Post data * @return boolean Can we edit it? @@ -32,7 +32,7 @@ protected function check_edit_permission( $post ) { } /** - * Check that the object can be accessed + * Check that the object can be accessed. * * @param mixed $id Object ID * @return boolean|WP_Error @@ -58,7 +58,7 @@ protected function check_object( $id ) { } /** - * Add meta to a post + * Add meta to a post. * * Ensures that the correct location header is sent with the response. * @@ -84,7 +84,7 @@ public function add_meta( $id, $data ) { } /** - * Add post meta to post responses + * Add post meta to post responses. * * Adds meta to post responses for the 'edit' context. * @@ -109,7 +109,7 @@ public function add_post_meta_data( $data, $post, $context ) { } /** - * Add post meta on post update + * Add post meta on post update. * * Handles adding/updating post meta when creating or updating posts. * @@ -131,7 +131,7 @@ public function insert_post_meta( $post, $data ) { } /** - * Call protected method from WP_JSON_Posts + * Call protected method from {@see WP_JSON_Posts}. * * WPAPI-1.2 deprecated a bunch of protected methods by moving them to this * class. This proxy method is added to call those methods. diff --git a/lib/class-wp-json-meta.php b/lib/class-wp-json-meta.php index 8f90b81823..61e1c4cf6c 100644 --- a/lib/class-wp-json-meta.php +++ b/lib/class-wp-json-meta.php @@ -1,28 +1,28 @@ base ) ) { @@ -36,7 +36,7 @@ public function __construct( WP_JSON_ResponseHandler $server ) { } /** - * Register the meta-related routes + * Register the meta-related routes. * * @param array $routes Existing routes * @return array Modified routes @@ -82,7 +82,7 @@ protected function get_parent_column() { } /** - * Retrieve custom fields for object + * Retrieve custom fields for object. * * @param int $id Object ID * @return (array[]|WP_Error) List of meta object data on success, WP_Error otherwise @@ -142,7 +142,7 @@ public function get_meta( $id, $mid ) { } /** - * Prepares meta data for return as an object + * Prepares meta data for return as an object. * * @param int $parent_id Object ID * @param stdClass $data Metadata row from database @@ -179,7 +179,7 @@ protected function prepare_meta( $parent_id, $data, $is_raw = false ) { } /** - * Update/add/delete meta for an object + * Update/add/delete meta for an object. * * Meta data is expected to be sent in the same format as it's output: * @@ -216,7 +216,7 @@ public function handle_inline_meta( $parent_id, $data ) { } /** - * Add meta to an object + * Add meta to an object. * * @param int $id Object ID * @param array $data { @@ -293,7 +293,7 @@ public function update_meta( $id, $mid, $data ) { } /** - * Check if the data provided is valid data + * Check if the data provided is valid data. * * Excludes serialized data from being sent via the API. * @@ -310,7 +310,7 @@ protected function is_valid_meta_data( $data ) { } /** - * Add meta to an object + * Add meta to an object. * * @param int $id Object ID * @param array $data { @@ -369,7 +369,7 @@ public function add_meta( $id, $data ) { } /** - * Delete meta from an object + * Delete meta from an object. * * @param int $id Object ID * @param int $mid Metadata ID From 961c83c26b56b68525f23174e023931b013c32d5 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 21 Jul 2014 20:04:40 -0500 Subject: [PATCH 382/515] Correct GET posts filter parameter to be posts_per_page. - Changed filter parameter to `posts_per_page` - Added example of filtering GET /posts/ - Intended to fix #357 --- docs/routes/routes.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/routes/routes.md b/docs/routes/routes.md index c13d7e9e8e..8e89d8e0ce 100644 --- a/docs/routes/routes.md +++ b/docs/routes/routes.md @@ -85,14 +85,15 @@ posts. ### Input #### `filter` -The `filter` parameter controls the query parameters. It is essentially a subset of the parameters available to [`WP_Query`](http://codex.wordpress.org/Class_Reference/WP_Query). +The `filter` parameter controls the query parameters. It is essentially a +subset of the parameters available to [`WP_Query`](http://codex.wordpress.org/Class_Reference/WP_Query). The parameter should be an array of the following key/value pairs: * `post_status` - Comma-separated list of [status values](http://codex.wordpress.org/Class_Reference/WP_Query#Status_Parameters). Default is "publish". (string) -* `numberposts` - Number of posts to retrieve, use `-1` for all posts. Default +* `posts_per_page` - Number of posts to retrieve, use `-1` for all posts. Default is set by the site. (integer) * `offset` - Number of posts to skip. Default is 0. (integer) * `orderby` - Parameter to search by, as per [WP Query](http://codex.wordpress.org/Class_Reference/WP_Query#Order_.26_Orderby_Parameters). Default is @@ -100,6 +101,9 @@ The parameter should be an array of the following key/value pairs: * `order` - Order to sort by. Default is "DESC". (string, "ASC" or "DESC") * `s` - Keyword to search for. (string) +``` +GET /posts?filter[posts_per_page]=8&filter[order]=ASC +``` #### `context` The `context` parameter controls the format of the data to return. See the From e9a59e86f39b946c63a9b6c9f7696c2b436aac37 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 27 Jul 2014 13:56:36 -0500 Subject: [PATCH 383/515] Renamed new_post method to create_post to match CRUD actions. --- lib/class-wp-json-posts.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 27ce5e69a6..eecd96584c 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -28,7 +28,7 @@ public function register_routes( $routes ) { // Post endpoints '/posts' => array( array( array( $this, 'get_posts' ), WP_JSON_Server::READABLE ), - array( array( $this, 'new_post' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), + array( array( $this, 'create_post' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), ), '/posts/(?P\d+)' => array( @@ -293,7 +293,7 @@ protected function check_edit_permission( $post ) { * - any other fields supported by wp_insert_post() * @return array Post data (see {@see WP_JSON_Posts::get_post}) */ - public function new_post( $data ) { + public function create_post( $data ) { unset( $data['ID'] ); $result = $this->insert_post( $data ); @@ -364,7 +364,7 @@ public function get_post( $id, $context = 'view' ) { * @internal 'data' is used here rather than 'content', as get_default_post_to_edit uses $_REQUEST['content'] * * @param int $id Post ID to edit - * @param array $data Data construct, see {@see WP_JSON_Posts::new_post} + * @param array $data Data construct, see {@see WP_JSON_Posts::create_post} * @param array $_headers Header data * @return true on success */ @@ -1253,7 +1253,7 @@ public function add_post_type_data( $data, $taxonomy, $context = 'view' ) { } /** - * Helper method for {@see new_post} and {@see edit_post}, containing shared logic. + * Helper method for {@see create_post} and {@see edit_post}, containing shared logic. * * @since 3.4.0 * @uses wp_insert_post() From df51c221bd7aef08bd82376a33630df85360c5c1 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 16 Jul 2014 16:18:56 +1000 Subject: [PATCH 384/515] Bump to 1.1.1 --- plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.php b/plugin.php index b5ed901a1b..04c0be125e 100644 --- a/plugin.php +++ b/plugin.php @@ -4,7 +4,7 @@ * Description: JSON-based REST API for WordPress, developed as part of GSoC 2013. * Author: Ryan McCue * Author URI: http://ryanmccue.info/ - * Version: 1.1 + * Version: 1.1.1 * Plugin URI: https://github.com/rmccue/WP-API */ @@ -13,7 +13,7 @@ * * @var string */ -define( 'JSON_API_VERSION', '1.1' ); +define( 'JSON_API_VERSION', '1.1.1' ); /** * Include our files for the API. From 841ab450e5ae62bc7b089bf847006cc3d625ee7c Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 27 Jul 2014 14:31:50 -0500 Subject: [PATCH 385/515] Include stub for deprecated new_post method. --- lib/class-wp-json-posts.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index eecd96584c..f9f936197b 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -308,6 +308,21 @@ public function create_post( $data ) { return $response; } + /** + * Create a new post for any registered post type. + * + * @deprecated + * @internal 'data' is used here rather than 'content', as get_default_post_to_edit uses $_REQUEST['content'] + * + * @param array $content Content data. (see {@see WP_JSON_Posts::create_post}) + * @return array Post data (see {@see WP_JSON_Posts::get_post}) + */ + public function new_post( $data ) { + _deprecated_function( __CLASS__ . '::' . __METHOD__, 'WPAPI-1.2', 'WP_JSON_Posts::create_post' ); + + return $this->create_post( $data ); + } + /** * Retrieve a post. * From 1b7583d29d8eb2583d785f8e06aea0bdd57d8a39 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 27 Jul 2014 14:37:09 -0500 Subject: [PATCH 386/515] Update method name to create_post in post tests. --- tests/test-json-posts.php | 122 +++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/tests/test-json-posts.php b/tests/test-json-posts.php index fcbaffefe9..bc20d2ba70 100644 --- a/tests/test-json-posts.php +++ b/tests/test-json-posts.php @@ -123,14 +123,14 @@ protected function check_create_response( $response ) { $this->assertArrayHasKey( 'Last-Modified', $headers ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); + $create_post = get_post( $response_data['ID'] ); - $this->check_get_post_response( $response, $new_post ); + $this->check_get_post_response( $response, $create_post ); } function test_create_post() { $data = $this->set_data(); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $this->check_create_response( $response ); } @@ -138,7 +138,7 @@ function test_create_post_other_type() { $data = $this->set_data(array( 'type' => 'page', )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $this->check_create_response( $response ); } @@ -146,7 +146,7 @@ function test_create_post_invalid_type() { $data = $this->set_data(array( 'type' => 'testposttype', )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $this->assertErrorResponse( 'json_invalid_post_type', $response, 400 ); } @@ -155,13 +155,13 @@ function test_create_post_other_author() { $data = $this->set_data(array( 'author' => $other_user, )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['author'], $new_post->post_author ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['author'], $create_post->post_author ); } function test_create_post_other_author_object() { @@ -171,20 +171,20 @@ function test_create_post_other_author_object() { 'ID' => $other_user, ), )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['author']->ID, $new_post->post_author ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['author']->ID, $create_post->post_author ); } function test_create_post_invalid_author() { $data = $this->set_data(array( 'author' => -1, )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $this->assertErrorResponse( 'json_invalid_author', $response, 400 ); } @@ -194,7 +194,7 @@ function test_create_post_invalid_author_object() { 'ID' => -1, ), )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $this->assertErrorResponse( 'json_invalid_author', $response, 400 ); } @@ -204,7 +204,7 @@ function test_create_post_invalid_author_object_id() { 'testfield' => 'testvalue', ), )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $this->assertErrorResponse( 'json_invalid_author', $response, 400 ); } @@ -214,7 +214,7 @@ function test_create_post_invalid_author_without_permission() { wp_set_current_user( $other_user ); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $this->assertErrorResponse( 'json_cannot_edit_others', $response, 401 ); } @@ -227,7 +227,7 @@ function test_create_post_without_permission() { $user->get_role_caps(); $user->update_user_level_from_caps(); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $this->assertErrorResponse( 'json_cannot_create', $response, 403 ); } @@ -236,13 +236,13 @@ function test_create_post_draft() { 'status' => 'draft', )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['status'], $new_post->post_status ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['status'], $create_post->post_status ); } function test_create_post_private() { @@ -250,13 +250,13 @@ function test_create_post_private() { 'status' => 'private', )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['status'], $new_post->post_status ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['status'], $create_post->post_status ); } function test_create_post_private_without_permission() { @@ -270,7 +270,7 @@ function test_create_post_private_without_permission() { $user->get_role_caps(); $user->update_user_level_from_caps(); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $this->assertErrorResponse( 'json_cannot_create_private', $response, 403 ); } @@ -285,7 +285,7 @@ function test_create_post_publish_without_permission() { $user->get_role_caps(); $user->update_user_level_from_caps(); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $this->assertErrorResponse( 'json_cannot_publish', $response, 403 ); } @@ -297,13 +297,13 @@ function test_create_post_invalid_status() { 'status' => 'teststatus', )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( 'draft', $new_post->post_status ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( 'draft', $create_post->post_status ); } function test_create_post_with_password() { @@ -312,13 +312,13 @@ function test_create_post_with_password() { 'password' => 'testing', )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['password'], $new_post->post_password ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['password'], $create_post->post_password ); } function test_create_post_with_password_without_permission() { @@ -333,7 +333,7 @@ function test_create_post_with_password_without_permission() { $user->get_role_caps(); $user->update_user_level_from_caps(); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $this->assertErrorResponse( 'json_cannot_publish', $response, 403 ); } @@ -346,14 +346,14 @@ function test_create_page_with_parent() { 'parent' => $parent, )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); + $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $parent, $new_post->post_parent ); + $this->assertEquals( $parent, $create_post->post_parent ); } function test_create_page_with_invalid_parent() { @@ -365,7 +365,7 @@ function test_create_page_with_invalid_parent() { 'parent' => -1, )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $this->assertErrorResponse( 'json_post_invalid_id', $response, 400 ); } @@ -375,13 +375,13 @@ function test_create_post_custom_date() { )); $time = gmmktime( 2, 0, 0, 1, 1, 2010 ); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $time, strtotime( $new_post->post_date ) ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $create_post->post_date ) ); } function test_create_post_custom_date_with_timezone() { @@ -390,13 +390,13 @@ function test_create_post_custom_date_with_timezone() { )); $time = gmmktime( 12, 0, 0, 1, 1, 2010 ); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $time, strtotime( $new_post->post_date ) ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $create_post->post_date ) ); } function test_create_post_custom_date_gmt() { @@ -405,13 +405,13 @@ function test_create_post_custom_date_gmt() { )); $time = gmmktime( 2, 0, 0, 1, 1, 2010 ); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $time, strtotime( $new_post->post_date ) ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $create_post->post_date ) ); } /** @@ -423,13 +423,13 @@ function test_create_post_custom_date_gmt_with_timezone() { )); $time = gmmktime( 2, 0, 0, 1, 1, 2010 ); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $time, strtotime( $new_post->post_date ) ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $create_post->post_date ) ); } /** @@ -442,13 +442,13 @@ function test_create_post_custom_modified() { )); $time = gmmktime( 2, 0, 0, 1, 1, 2010 ); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $time, strtotime( $new_post->post_modified ) ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $create_post->post_modified ) ); } function test_create_post_menu_order() { @@ -456,13 +456,13 @@ function test_create_post_menu_order() { 'menu_order' => 5, )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['menu_order'], $new_post->menu_order ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['menu_order'], $create_post->menu_order ); } function test_create_post_menu_order_negative() { @@ -470,13 +470,13 @@ function test_create_post_menu_order_negative() { 'menu_order' => -5, )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['menu_order'], $new_post->menu_order ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['menu_order'], $create_post->menu_order ); } function test_create_post_menu_order_noninteger() { @@ -484,13 +484,13 @@ function test_create_post_menu_order_noninteger() { 'menu_order' => 'test', )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( 0, $new_post->menu_order ); + $create_post = get_post( $response_data['ID'] ); + $this->assertEquals( 0, $create_post->menu_order ); } function test_create_post_sticky() { @@ -498,7 +498,7 @@ function test_create_post_sticky() { 'sticky' => true, )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); @@ -511,7 +511,7 @@ function test_create_post_sticky_false() { 'sticky' => false, )); - $response = $this->endpoint->new_post( $data ); + $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); $this->check_create_response( $response ); From 469a9c3a2d84f2aa136b30da280ad7b2c600d5a5 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 27 Jul 2014 14:55:06 -0500 Subject: [PATCH 387/515] Rename new_user method to create_user to match CRUD actions. Replaced new_user method name with create_user. Added stub method with deprecated notice for new_user. --- lib/class-wp-json-users.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index c9717a5ac0..d60f3757f0 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -28,7 +28,7 @@ public function register_routes( $routes ) { // User endpoints '/users' => array( array( array( $this, 'get_users' ), WP_JSON_Server::READABLE ), - array( array( $this, 'new_user' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), + array( array( $this, 'create_user' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), ), '/users/(?P\d+)' => array( array( array( $this, 'get_user' ), WP_JSON_Server::READABLE ), @@ -373,7 +373,7 @@ public function edit_user( $id, $data, $_headers = array() ) { * @param $data * @return mixed */ - public function new_user( $data ) { + public function create_user( $data ) { if ( ! current_user_can( 'create_users' ) ) { return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to create users.' ), array( 'status' => 403 ) ); } @@ -400,6 +400,20 @@ public function new_user( $data ) { return $response; } + /** + * Create a new user. + * + * @deprecated + * + * @param $data + * @return mixed + */ + public function create_user( $data ) { + _deprecated_function( __CLASS__ . '::' . __METHOD__, 'WPAPI-1.2', 'WP_JSON_Users::create_user' ); + + return $this->create_user( $data ); + } + /** * Delete a user. * From 7c06f6e74ff1913f8265136378171795467bac0b Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 27 Jul 2014 15:00:51 -0500 Subject: [PATCH 388/515] Update method name to create_user in user tests --- tests/test-json-users.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test-json-users.php b/tests/test-json-users.php index 5b0ea1b84c..ada33f0a16 100644 --- a/tests/test-json-users.php +++ b/tests/test-json-users.php @@ -91,7 +91,7 @@ public function test_create_user() { 'password' => 'test_password', 'email' => 'test@example.com', ); - $response = $this->endpoint->new_user( $data ); + $response = $this->endpoint->create_user( $data ); // Check that we succeeded $this->assertNotInstanceOf( 'WP_Error', $response ); @@ -100,15 +100,15 @@ public function test_create_user() { $response_data = $response->get_data(); // Check that the data is intact - $new_user = get_userdata( $response_data['ID'] ); + $create_user = get_userdata( $response_data['ID'] ); $this->assertEquals( $data['username'], $response_data['username'] ); - $this->assertEquals( $data['username'], $new_user->user_login ); + $this->assertEquals( $data['username'], $create_user->user_login ); $this->assertEquals( false, $response_data['email'] ); - $this->assertEquals( $data['email'], $new_user->user_email ); + $this->assertEquals( $data['email'], $create_user->user_email ); - $this->assertTrue( wp_check_password( $data['password'], $new_user->user_pass ), 'Password check failed' ); + $this->assertTrue( wp_check_password( $data['password'], $create_user->user_pass ), 'Password check failed' ); } public function test_create_user_missing_params() { @@ -116,7 +116,7 @@ public function test_create_user_missing_params() { $data = array( 'username' => 'test_user', ); - $response = $this->endpoint->new_user( $data ); + $response = $this->endpoint->create_user( $data ); $this->assertInstanceOf( 'WP_Error', $response ); } From 374b19e4c42409761a85ed3148eae688a8d9eaad Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 27 Jul 2014 15:07:56 -0500 Subject: [PATCH 389/515] Rename new_post route in custom post type class to match parent class create_post method. --- lib/class-wp-json-customposttype.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-customposttype.php b/lib/class-wp-json-customposttype.php index 29fc7f9699..997fee7248 100644 --- a/lib/class-wp-json-customposttype.php +++ b/lib/class-wp-json-customposttype.php @@ -60,8 +60,8 @@ public function register_filters() { */ public function register_routes( $routes ) { $routes[ $this->base ] = array( - array( array( $this, 'get_posts' ), WP_JSON_Server::READABLE ), - array( array( $this, 'new_post' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), + array( array( $this, 'get_posts' ), WP_JSON_Server::READABLE ), + array( array( $this, 'create_post' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), ); $routes[ $this->base . '/(?P\d+)' ] = array( From b83e8b33fe74dc439c9b9f1e8d477d0e285fb781 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 27 Jul 2014 15:11:10 -0500 Subject: [PATCH 390/515] Fix stub method name of what should be new_user --- lib/class-wp-json-users.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index d60f3757f0..13bc1c6a8c 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -408,7 +408,7 @@ public function create_user( $data ) { * @param $data * @return mixed */ - public function create_user( $data ) { + public function new_user( $data ) { _deprecated_function( __CLASS__ . '::' . __METHOD__, 'WPAPI-1.2', 'WP_JSON_Users::create_user' ); return $this->create_user( $data ); From e7d8f2557db0d569373a8702bdeef84d9ceb3739 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 27 Jul 2014 15:20:41 -0500 Subject: [PATCH 391/515] Revert test variable names in post and user tests. --- tests/test-json-posts.php | 64 +++++++++++++++++++-------------------- tests/test-json-users.php | 8 ++--- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/tests/test-json-posts.php b/tests/test-json-posts.php index bc20d2ba70..aa0c690002 100644 --- a/tests/test-json-posts.php +++ b/tests/test-json-posts.php @@ -123,9 +123,9 @@ protected function check_create_response( $response ) { $this->assertArrayHasKey( 'Last-Modified', $headers ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); + $new_post = get_post( $response_data['ID'] ); - $this->check_get_post_response( $response, $create_post ); + $this->check_get_post_response( $response, $new_post ); } function test_create_post() { @@ -160,8 +160,8 @@ function test_create_post_other_author() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['author'], $create_post->post_author ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['author'], $new_post->post_author ); } function test_create_post_other_author_object() { @@ -176,8 +176,8 @@ function test_create_post_other_author_object() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['author']->ID, $create_post->post_author ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['author']->ID, $new_post->post_author ); } function test_create_post_invalid_author() { @@ -241,8 +241,8 @@ function test_create_post_draft() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['status'], $create_post->post_status ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['status'], $new_post->post_status ); } function test_create_post_private() { @@ -255,8 +255,8 @@ function test_create_post_private() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['status'], $create_post->post_status ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['status'], $new_post->post_status ); } function test_create_post_private_without_permission() { @@ -302,8 +302,8 @@ function test_create_post_invalid_status() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( 'draft', $create_post->post_status ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( 'draft', $new_post->post_status ); } function test_create_post_with_password() { @@ -317,8 +317,8 @@ function test_create_post_with_password() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['password'], $create_post->post_password ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['password'], $new_post->post_password ); } function test_create_post_with_password_without_permission() { @@ -351,9 +351,9 @@ function test_create_page_with_parent() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); + $new_post = get_post( $response_data['ID'] ); - $this->assertEquals( $parent, $create_post->post_parent ); + $this->assertEquals( $parent, $new_post->post_parent ); } function test_create_page_with_invalid_parent() { @@ -380,8 +380,8 @@ function test_create_post_custom_date() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $time, strtotime( $create_post->post_date ) ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $new_post->post_date ) ); } function test_create_post_custom_date_with_timezone() { @@ -395,8 +395,8 @@ function test_create_post_custom_date_with_timezone() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $time, strtotime( $create_post->post_date ) ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $new_post->post_date ) ); } function test_create_post_custom_date_gmt() { @@ -410,8 +410,8 @@ function test_create_post_custom_date_gmt() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $time, strtotime( $create_post->post_date ) ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $new_post->post_date ) ); } /** @@ -428,8 +428,8 @@ function test_create_post_custom_date_gmt_with_timezone() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $time, strtotime( $create_post->post_date ) ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $new_post->post_date ) ); } /** @@ -447,8 +447,8 @@ function test_create_post_custom_modified() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $time, strtotime( $create_post->post_modified ) ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $time, strtotime( $new_post->post_modified ) ); } function test_create_post_menu_order() { @@ -461,8 +461,8 @@ function test_create_post_menu_order() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['menu_order'], $create_post->menu_order ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['menu_order'], $new_post->menu_order ); } function test_create_post_menu_order_negative() { @@ -475,8 +475,8 @@ function test_create_post_menu_order_negative() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( $data['menu_order'], $create_post->menu_order ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( $data['menu_order'], $new_post->menu_order ); } function test_create_post_menu_order_noninteger() { @@ -489,8 +489,8 @@ function test_create_post_menu_order_noninteger() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $create_post = get_post( $response_data['ID'] ); - $this->assertEquals( 0, $create_post->menu_order ); + $new_post = get_post( $response_data['ID'] ); + $this->assertEquals( 0, $new_post->menu_order ); } function test_create_post_sticky() { diff --git a/tests/test-json-users.php b/tests/test-json-users.php index ada33f0a16..8bd938c951 100644 --- a/tests/test-json-users.php +++ b/tests/test-json-users.php @@ -100,15 +100,15 @@ public function test_create_user() { $response_data = $response->get_data(); // Check that the data is intact - $create_user = get_userdata( $response_data['ID'] ); + $new_user = get_userdata( $response_data['ID'] ); $this->assertEquals( $data['username'], $response_data['username'] ); - $this->assertEquals( $data['username'], $create_user->user_login ); + $this->assertEquals( $data['username'], $new_user->user_login ); $this->assertEquals( false, $response_data['email'] ); - $this->assertEquals( $data['email'], $create_user->user_email ); + $this->assertEquals( $data['email'], $new_user->user_email ); - $this->assertTrue( wp_check_password( $data['password'], $create_user->user_pass ), 'Password check failed' ); + $this->assertTrue( wp_check_password( $data['password'], $new_user->user_pass ), 'Password check failed' ); } public function test_create_user_missing_params() { From 776a901694a14b8135fd9f2ade0db2e9201274b7 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 27 Jul 2014 15:39:08 -0500 Subject: [PATCH 392/515] Split comments into separate class --- lib/class-wp-json-comments.php | 231 +++++++++++++++++++++++++++++++++ lib/class-wp-json-posts.php | 216 +----------------------------- plugin.php | 1 + 3 files changed, 236 insertions(+), 212 deletions(-) create mode 100644 lib/class-wp-json-comments.php diff --git a/lib/class-wp-json-comments.php b/lib/class-wp-json-comments.php new file mode 100644 index 0000000000..762268fae5 --- /dev/null +++ b/lib/class-wp-json-comments.php @@ -0,0 +1,231 @@ +\d+)/meta). Must include ID selector. + */ + protected $base = '/posts/(?P\d+)/comments'; + + /** + * Register the comment-related routes + * + * @param array $routes Existing routes + * @return array Modified routes + */ + public function register_routes( $routes ) { + $routes[ $this->base ] = array( + array( array( $this, 'get_comments' ), WP_JSON_Server::READABLE ), + ); + $routes[ $this->base . '/(?P\d+)'] = array( + array( array( $this, 'get_comment' ), WP_JSON_Server::READABLE ), + array( array( $this, 'delete_comment' ), WP_JSON_Server::DELETABLE ), + ); + + return $routes; + } + + /** + * Delete a comment. + * + * @uses wp_delete_comment + * @param int $id Post ID + * @param int $comment Comment ID + * @param boolean $force Skip trash + * @return array + */ + public function delete_comment( $id, $comment, $force = false ) { + $comment = (int) $comment; + + if ( empty( $comment ) ) { + return new WP_Error( 'json_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); + } + + $comment_array = get_comment( $comment, ARRAY_A ); + + if ( empty( $comment_array ) ) { + return new WP_Error( 'json_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); + } + + if ( ! current_user_can( 'edit_comment', $comment_array['comment_ID'] ) ) { + return new WP_Error( 'json_user_cannot_delete_comment', __( 'Sorry, you are not allowed to delete this comment.' ), array( 'status' => 401 ) ); + } + + $result = wp_delete_comment( $comment_array['comment_ID'], $force ); + + if ( ! $result ) { + return new WP_Error( 'json_cannot_delete', __( 'The comment cannot be deleted.' ), array( 'status' => 500 ) ); + } + + if ( $force ) { + return array( 'message' => __( 'Permanently deleted comment' ) ); + } else { + // TODO: return a HTTP 202 here instead + return array( 'message' => __( 'Deleted comment' ) ); + } + } + + /** + * Retrieve comments + * + * @param int $id Post ID to retrieve comments for + * @return array List of Comment entities + */ + public function get_comments( $id ) { + //$args = array('status' => $status, 'post_id' => $id, 'offset' => $offset, 'number' => $number )l + $comments = get_comments( array('post_id' => $id) ); + + $post = get_post( $id, ARRAY_A ); + + if ( empty( $post['ID'] ) ) { + return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); + } + + if ( ! $this->check_read_permission( $post ) ) { + return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); + } + + $struct = array(); + + foreach ( $comments as $comment ) { + $struct[] = $this->prepare_comment( $comment, array( 'comment', 'meta' ), 'collection' ); + } + + return $struct; + } + + /** + * Retrieve a single comment + * + * @param int $comment Comment ID + * @return array Comment entity + */ + public function get_comment( $comment ) { + $comment = get_comment( $comment ); + + if ( empty( $comment ) ) { + return new WP_Error( 'json_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); + } + + $data = $this->prepare_comment( $comment ); + + return $data; + } + + /** + * Prepares comment data for returning as a JSON response. + * + * @param stdClass $comment Comment object + * @param array $requested_fields Fields to retrieve from the comment + * @param string $context Where is the comment being loaded? + * @return array Comment data for JSON serialization + */ + protected function prepare_comment( $comment, $requested_fields = array( 'comment', 'meta' ), $context = 'single' ) { + $fields = array( + 'ID' => (int) $comment->comment_ID, + 'post' => (int) $comment->comment_post_ID, + ); + + $post = (array) get_post( $fields['post'] ); + + // Content + $fields['content'] = apply_filters( 'comment_text', $comment->comment_content, $comment ); + // $fields['content_raw'] = $comment->comment_content; + + // Status + switch ( $comment->comment_approved ) { + case 'hold': + case '0': + $fields['status'] = 'hold'; + break; + + case 'approve': + case '1': + $fields['status'] = 'approved'; + break; + + case 'spam': + case 'trash': + default: + $fields['status'] = $comment->comment_approved; + break; + } + + // Type + $fields['type'] = apply_filters( 'get_comment_type', $comment->comment_type ); + + if ( empty( $fields['type'] ) ) { + $fields['type'] = 'comment'; + } + + // Post + if ( 'single' === $context ) { + $parent = get_post( $post['post_parent'], ARRAY_A ); + $fields['parent'] = $this->prepare_post( $parent, 'single-parent' ); + } + + // Parent + if ( ( 'single' === $context || 'single-parent' === $context ) && (int) $comment->comment_parent ) { + $parent_fields = array( 'meta' ); + + if ( $context === 'single' ) { + $parent_fields[] = 'comment'; + } + $parent = get_comment( $post['post_parent'] ); + + $fields['parent'] = $this->prepare_comment( $parent, $parent_fields, 'single-parent' ); + } + + // Parent + $fields['parent'] = (int) $comment->comment_parent; + + // Author + if ( (int) $comment->user_id !== 0 ) { + $fields['author'] = (int) $comment->user_id; + } else { + $fields['author'] = array( + 'ID' => 0, + 'name' => $comment->comment_author, + 'URL' => $comment->comment_author_url, + 'avatar' => json_get_avatar_url( $comment->comment_author_email ), + ); + } + + // Date + $timezone = json_get_timezone(); + + $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $comment->comment_date, $timezone ); + $fields['date'] = $date->format( 'c' ); + $fields['date_tz'] = $date->format( 'e' ); + $fields['date_gmt'] = date( 'c', strtotime( $comment->comment_date_gmt ) ); + + // Meta + $meta = array( + 'links' => array( + 'up' => json_url( sprintf( '/posts/%d', (int) $comment->comment_post_ID ) ) + ), + ); + + if ( 0 !== (int) $comment->comment_parent ) { + $meta['links']['in-reply-to'] = json_url( sprintf( '/posts/%d/comments/%d', (int) $comment->comment_post_ID, (int) $comment->comment_parent ) ); + } + + if ( 'single' !== $context ) { + $meta['links']['self'] = json_url( sprintf( '/posts/%d/comments/%d', (int) $comment->comment_post_ID, (int) $comment->comment_ID ) ); + } + + // Remove unneeded fields + $data = array(); + + if ( in_array( 'comment', $requested_fields ) ) { + $data = array_merge( $data, $fields ); + } + + if ( in_array( 'meta', $requested_fields ) ) { + $data['meta'] = $meta; + } + + return apply_filters( 'json_prepare_comment', $data, $comment, $context ); + } +} diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index f9f936197b..a230bc75a0 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -15,6 +15,8 @@ class WP_JSON_Posts { */ public function __construct(WP_JSON_ResponseHandler $server) { $this->server = $server; + + $this->comments = new WP_JSON_Comments(); } /** @@ -51,15 +53,6 @@ public function register_routes( $routes ) { array( array( $this, 'delete_meta' ), WP_JSON_Server::DELETABLE ), ), - // Comments - '/posts/(?P\d+)/comments' => array( - array( array( $this, 'get_comments' ), WP_JSON_Server::READABLE ), - ), - '/posts/(?P\d+)/comments/(?P\d+)' => array( - array( array( $this, 'get_comment' ), WP_JSON_Server::READABLE ), - array( array( $this, 'delete_comment' ), WP_JSON_Server::DELETABLE ), - ), - // Meta-post endpoints '/posts/types' => array( array( $this, 'get_post_types' ), WP_JSON_Server::READABLE @@ -71,6 +64,8 @@ public function register_routes( $routes ) { array( $this, 'get_post_statuses' ), WP_JSON_Server::READABLE ), ); + $post_routes = $this->comments->register_routes( $post_routes ); + return array_merge( $routes, $post_routes ); } @@ -465,93 +460,6 @@ public function delete_post( $id, $force = false ) { } } - /** - * Delete a comment. - * - * @uses wp_delete_comment - * @param int $id Post ID - * @param int $comment Comment ID - * @param boolean $force Skip trash - * @return array - */ - public function delete_comment( $id, $comment, $force = false ) { - $comment = (int) $comment; - - if ( empty( $comment ) ) { - return new WP_Error( 'json_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); - } - - $comment_array = get_comment( $comment, ARRAY_A ); - - if ( empty( $comment_array ) ) { - return new WP_Error( 'json_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); - } - - if ( ! current_user_can( 'edit_comment', $comment_array['comment_ID'] ) ) { - return new WP_Error( 'json_user_cannot_delete_comment', __( 'Sorry, you are not allowed to delete this comment.' ), array( 'status' => 401 ) ); - } - - $result = wp_delete_comment( $comment_array['comment_ID'], $force ); - - if ( ! $result ) { - return new WP_Error( 'json_cannot_delete', __( 'The comment cannot be deleted.' ), array( 'status' => 500 ) ); - } - - if ( $force ) { - return array( 'message' => __( 'Permanently deleted comment' ) ); - } else { - // TODO: return a HTTP 202 here instead - return array( 'message' => __( 'Deleted comment' ) ); - } - } - - /** - * Retrieve comments - * - * @param int $id Post ID to retrieve comments for - * @return array List of Comment entities - */ - public function get_comments( $id ) { - //$args = array('status' => $status, 'post_id' => $id, 'offset' => $offset, 'number' => $number )l - $comments = get_comments( array('post_id' => $id) ); - - $post = get_post( $id, ARRAY_A ); - - if ( empty( $post['ID'] ) ) { - return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - } - - if ( ! $this->check_read_permission( $post ) ) { - return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); - } - - $struct = array(); - - foreach ( $comments as $comment ) { - $struct[] = $this->prepare_comment( $comment, array( 'comment', 'meta' ), 'collection' ); - } - - return $struct; - } - - /** - * Retrieve a single comment - * - * @param int $comment Comment ID - * @return array Comment entity - */ - public function get_comment( $comment ) { - $comment = get_comment( $comment ); - - if ( empty( $comment ) ) { - return new WP_Error( 'json_comment_invalid_id', __( 'Invalid comment ID.' ), array( 'status' => 404 ) ); - } - - $data = $this->prepare_comment( $comment ); - - return $data; - } - /** * Get all public post types * @@ -1516,120 +1424,4 @@ protected function insert_post( $data ) { return $post_ID; } - - /** - * Prepares comment data for returning as a JSON response. - * - * @param stdClass $comment Comment object - * @param array $requested_fields Fields to retrieve from the comment - * @param string $context Where is the comment being loaded? - * @return array Comment data for JSON serialization - */ - protected function prepare_comment( $comment, $requested_fields = array( 'comment', 'meta' ), $context = 'single' ) { - $fields = array( - 'ID' => (int) $comment->comment_ID, - 'post' => (int) $comment->comment_post_ID, - ); - - $post = (array) get_post( $fields['post'] ); - - // Content - $fields['content'] = apply_filters( 'comment_text', $comment->comment_content, $comment ); - // $fields['content_raw'] = $comment->comment_content; - - // Status - switch ( $comment->comment_approved ) { - case 'hold': - case '0': - $fields['status'] = 'hold'; - break; - - case 'approve': - case '1': - $fields['status'] = 'approved'; - break; - - case 'spam': - case 'trash': - default: - $fields['status'] = $comment->comment_approved; - break; - } - - // Type - $fields['type'] = apply_filters( 'get_comment_type', $comment->comment_type ); - - if ( empty( $fields['type'] ) ) { - $fields['type'] = 'comment'; - } - - // Post - if ( 'single' === $context ) { - $parent = get_post( $post['post_parent'], ARRAY_A ); - $fields['parent'] = $this->prepare_post( $parent, 'single-parent' ); - } - - // Parent - if ( ( 'single' === $context || 'single-parent' === $context ) && (int) $comment->comment_parent ) { - $parent_fields = array( 'meta' ); - - if ( $context === 'single' ) { - $parent_fields[] = 'comment'; - } - $parent = get_comment( $post['post_parent'] ); - - $fields['parent'] = $this->prepare_comment( $parent, $parent_fields, 'single-parent' ); - } - - // Parent - $fields['parent'] = (int) $comment->comment_parent; - - // Author - if ( (int) $comment->user_id !== 0 ) { - $fields['author'] = (int) $comment->user_id; - } else { - $fields['author'] = array( - 'ID' => 0, - 'name' => $comment->comment_author, - 'URL' => $comment->comment_author_url, - 'avatar' => json_get_avatar_url( $comment->comment_author_email ), - ); - } - - // Date - $timezone = json_get_timezone(); - - $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $comment->comment_date, $timezone ); - $fields['date'] = $date->format( 'c' ); - $fields['date_tz'] = $date->format( 'e' ); - $fields['date_gmt'] = date( 'c', strtotime( $comment->comment_date_gmt ) ); - - // Meta - $meta = array( - 'links' => array( - 'up' => json_url( sprintf( '/posts/%d', (int) $comment->comment_post_ID ) ) - ), - ); - - if ( 0 !== (int) $comment->comment_parent ) { - $meta['links']['in-reply-to'] = json_url( sprintf( '/posts/%d/comments/%d', (int) $comment->comment_post_ID, (int) $comment->comment_parent ) ); - } - - if ( 'single' !== $context ) { - $meta['links']['self'] = json_url( sprintf( '/posts/%d/comments/%d', (int) $comment->comment_post_ID, (int) $comment->comment_ID ) ); - } - - // Remove unneeded fields - $data = array(); - - if ( in_array( 'comment', $requested_fields ) ) { - $data = array_merge( $data, $fields ); - } - - if ( in_array( 'meta', $requested_fields ) ) { - $data['meta'] = $meta; - } - - return apply_filters( 'json_prepare_comment', $data, $comment, $context ); - } } diff --git a/plugin.php b/plugin.php index 04c0be125e..0e3ca4ae69 100644 --- a/plugin.php +++ b/plugin.php @@ -29,6 +29,7 @@ include_once( dirname( __FILE__ ) . '/lib/class-wp-json-posts.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-users.php' ); +include_once( dirname( __FILE__ ) . '/lib/class-wp-json-comments.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-customposttype.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-pages.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-media.php' ); From e10c19d2d1a5428fe97d26a9a7df276970c62c5e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 27 Jul 2014 16:17:25 -0500 Subject: [PATCH 393/515] Add deprecated shim comment methods --- lib/class-wp-json-comments.php | 14 ++++++++ lib/class-wp-json-posts.php | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/lib/class-wp-json-comments.php b/lib/class-wp-json-comments.php index 762268fae5..868cd2a83a 100644 --- a/lib/class-wp-json-comments.php +++ b/lib/class-wp-json-comments.php @@ -228,4 +228,18 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen return apply_filters( 'json_prepare_comment', $data, $comment, $context ); } + + /** + * Call protected method from {@see WP_JSON_Posts}. + * + * WPAPI-1.2 deprecated a bunch of protected methods by moving them to this + * class. This proxy method is added to call those methods. + * + * @param string $method Method name + * @param array $args Method arguments + * @return mixed Return value from the method + */ + public function _deprecated_call( $method, $args ) { + return call_user_func_array( array( $this, $method ), $args ); + } } diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index a230bc75a0..5b3a3c4c6c 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1424,4 +1424,62 @@ protected function insert_post( $data ) { return $post_ID; } + + /** + * Delete a comment. + * + * @deprecated WPAPI-1.2 + * + * @param int $id + * @param int $comment + * @param boolean $force + * @return array|WP_Error + */ + public function delete_comment( $id, $comment, $force = false ) { + _deprecated_function( __CLASS__ . '::' . __METHOD__, 'WPAPI-1.2', 'WP_JSON_Comments::delete_comment' ); + + return $this->comments->delete_comment( $id, $comment, $force ); + } + + /** + * Retrieve comments. + * + * @deprecated WPAPI-1.2 + * + * @param int $id + * @return array + */ + public function get_comments( $id ) { + _deprecated_function( __CLASS__ . '::' . __METHOD__, 'WPAPI-1.2', 'WP_JSON_Comments::get_comments' ); + + return $this->comments->get_comments( $id ); + } + + /** + * Retrieve comments. + * + * @deprecated WPAPI-1.2 + * + * @param int $id + * @return array + */ + public function get_comment( $comment ) { + _deprecated_function( __CLASS__ . '::' . __METHOD__, 'WPAPI-1.2', 'WP_JSON_Comments::get_comment' ); + + return $this->comments->get_comment( $comment ); + } + + /** + * Prepares comment data for returning as a JSON response. + * + * @param stdClass $comment + * @param array $requested_fields + * @param string $context + * @return array + */ + protected function prepare_comment( $comment, $requested_fields = array( 'comment', 'meta' ), $context = 'single' ) { + _deprecated_function( __CLASS__ . '::' . __METHOD__, 'WPAPI-1.2', 'WP_JSON_Comments::prepare_comment' ); + + return $this->comments->_deprecated_call( 'prepare_comment', array( $comment, $requested_fields, $context ) ); + } } From 2d503ea2efc71a3a1bbefc6ac34aeaced55f69d0 Mon Sep 17 00:00:00 2001 From: Simon Lampen Date: Tue, 29 Jul 2014 12:21:10 +1200 Subject: [PATCH 394/515] Fix jsonp content-type response header Incorrectly sends application/json rather than application/javascript on a jsonp request which breaks strict mime handling. In response to error: Refused to execute script from '...url...' because its MIME type ('application/json') is not executable, and strict MIME type checking is enabled. --- lib/class-wp-json-server.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index ae2b76bc12..c6a88468f7 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -180,7 +180,8 @@ protected function json_error( $code, $message, $status = null ) { * @uses WP_JSON_Server::dispatch() */ public function serve_request( $path = null ) { - $this->send_header( 'Content-Type', 'application/json; charset=' . get_option( 'blog_charset' ), true ); + $content_type = isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json'; + $this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ), true ); // Mitigate possible JSONP Flash attacks // http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ From f116c98cb2881f8951eecf0506825b85f37d0573 Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Fri, 1 Aug 2014 13:16:26 -0400 Subject: [PATCH 395/515] Alter installation steps to use wp-cli for plugin & permalink setup WP-CLI is available within Chassis, so using it (via vagrant ssh) to activate the plugin and set permalinks makes setup slightly faster --- README.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5302b85ca0..03d67df609 100644 --- a/README.md +++ b/README.md @@ -69,20 +69,30 @@ testing environment in a few easy steps: vagrant up ``` -5. Browse to http://vagrant.local/wp/wp-admin/plugins.php and activate the WP -API plugin +5. Activate the plugin: + ```bash + vagrant ssh -c 'cd /vagrant && wp plugin activate json-rest-api' ``` - Username: admin - Password: password + +6. Set the permalink structure to something other than the default, in order to + enable the http://vagrant.local/wp-json/ endpoint URL (if you skip this + step, it can be accessed at http://vagrant.local/?json_route=/): + + ```bash + vagrant ssh -c "cd /vagrant && wp rewrite structure '/%postname%/'" ``` -6. Browse to http://vagrant.local/wp/wp-admin/options-permalink.php and set -the permalink structure to anything other than "Default" +You're done! You should now have a WordPress site available at +http://vagrant.local; you can access the API via http://vagrant.local/wp-json/ -7. Browse to http://vagrant.local/wp-json/ (or if the permalink structure is -still "Default," to http://vagrant.local/?json_route=/) +To access the admin interface, visit http://vagrant.local/wp/wp-admin and log +in with the credentials below: + ``` + Username: admin + Password: password + ``` ### Testing From 7d6f6ddced667c83257b692eff649d3a0e5062a3 Mon Sep 17 00:00:00 2001 From: Max Cutler Date: Fri, 1 Aug 2014 13:31:51 -0400 Subject: [PATCH 396/515] post 'parent' value should be null rather than zero if unset Having a field be either an integer or a nested object makes parsing more complicated. Furthermore, knowing that a parent ID of "0" means that a post has no parent is WordPress inside knowledge that should not leak through the API. --- lib/class-wp-json-posts.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index f9f936197b..1cefcf9bed 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -786,6 +786,10 @@ protected function prepare_post( $post, $context = 'view' ) { if ( empty( $post_fields['format'] ) ) { $post_fields['format'] = 'standard'; } + + if ( 0 === $post['post_parent'] ) { + $post_fields['parent'] = null; + } if ( ( 'view' === $context || 'view-revision' == $context ) && 0 !== $post['post_parent'] ) { // Avoid nesting too deeply From 331a08863f4c3d5dfa0761edb7f5cad81a4bf783 Mon Sep 17 00:00:00 2001 From: Pippin Williamson Date: Fri, 1 Aug 2014 13:35:48 -0400 Subject: [PATCH 397/515] Add support for passing the role parameter to insert_user() method. #335 --- lib/class-wp-json-users.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 13bc1c6a8c..448a34359f 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -307,6 +307,11 @@ protected function insert_user( $data ) { $user->user_email = $data['email']; } + // Role + if ( ! empty( $data['role'] ) ) { + $user->role = $data['role']; + } + // Pre-flight check $user = apply_filters( 'json_pre_insert_user', $user, $data ); From b5309ddbae5354887f5aa6e825175d743bd1a9b9 Mon Sep 17 00:00:00 2001 From: Pippin Williamson Date: Fri, 1 Aug 2014 13:59:23 -0400 Subject: [PATCH 398/515] Add license.txt file with license terms for #384 --- license.txt | 281 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 license.txt diff --git a/license.txt b/license.txt new file mode 100644 index 0000000000..211bee3c5f --- /dev/null +++ b/license.txt @@ -0,0 +1,281 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110, USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + From 37159366cd2e4cab5c5bd420f01c02272d6e2bb0 Mon Sep 17 00:00:00 2001 From: ironpaperweight Date: Fri, 1 Aug 2014 14:26:19 -0400 Subject: [PATCH 399/515] Update README.md Update Testing procedures to include the running provision step before running the testing suite --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 03d67df609..a9b2fa911e 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,13 @@ For testing, you'll need a little bit more: # From your base directory, api-tester if following the steps from before git clone --recursive https://github.com/Chassis/Tester.git extensions/tester ``` + +2. Run the provisioner: + ``` + vagrant provision + ``` + 3. Run the testing suite: ```bash From e4cae9a7497ffb611ef950e0cd12412ac3aacaa8 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Fri, 1 Aug 2014 14:27:34 -0400 Subject: [PATCH 400/515] Offload WordPress installation to bash script so it can be reused --- .travis.yml | 12 +------ bin/install-wp-tests.sh | 78 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 11 deletions(-) create mode 100755 bin/install-wp-tests.sh diff --git a/.travis.yml b/.travis.yml index 05aff999e5..8c5714da34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,16 +11,6 @@ php: # Clones WordPress and configures our testing environment. before_script: - - export PLUGIN_SLUG=$(basename $(pwd)) - - git clone --depth=1 git://develop.git.wordpress.org/ /tmp/wordpress - - mkdir "/tmp/wordpress/src/wp-content/plugins/$PLUGIN_SLUG" - - cp -r . "/tmp/wordpress/src/wp-content/plugins/$PLUGIN_SLUG/" - - cd /tmp/wordpress - - mysql -e "CREATE DATABASE wordpress_tests;" -uroot - - cp wp-tests-config-sample.php wp-tests-config.php - - sed -i "s/youremptytestdbnamehere/wordpress_tests/" wp-tests-config.php - - sed -i "s/yourusernamehere/travis/" wp-tests-config.php - - sed -i "s/yourpasswordhere//" wp-tests-config.php - - cd "/tmp/wordpress/src/wp-content/plugins/$PLUGIN_SLUG" + - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION script: phpunit diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100755 index 0000000000..1e39064a59 --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 [db-host] [wp-version]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} + +WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} +WP_CORE_DIR=/tmp/wordpress/ + +set -ex + +install_wp() { + mkdir -p $WP_CORE_DIR + + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + + wget -nv -O /tmp/wordpress.tar.gz http://wordpress.org/${ARCHIVE_NAME}.tar.gz + tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR + + wget -nv -O $WP_CORE_DIR/wp-content/db.php https://raw.github.com/markoheijnen/wp-mysqli/master/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i .bak' + else + local ioption='-i' + fi + + # set up testing suite + mkdir -p $WP_TESTS_DIR + cd $WP_TESTS_DIR + svn co --quiet http://develop.svn.wordpress.org/trunk/tests/phpunit/includes/ + + wget -nv -O wp-tests-config.php http://develop.svn.wordpress.org/trunk/wp-tests-config-sample.php + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" wp-tests-config.php +} + +install_db() { + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [[ "$DB_SOCK_OR_PORT" =~ ^[0-9]+$ ]] ; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db From 6fff237ec9647e9af06a2a2b4509a34d5e2355f0 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Fri, 1 Aug 2014 14:28:12 -0400 Subject: [PATCH 401/515] Run tests against single site and multisite --- .travis.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8c5714da34..9e3862a7f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,10 +4,14 @@ language: php php: - - "5.2" - - "5.3" - - "5.4" - - "5.5" + - 5.2 + - 5.3 + - 5.4 + - 5.5 + +env: + - WP_VERSION=latest WP_MULTISITE=0 + - WP_VERSION=latest WP_MULTISITE=1 # Clones WordPress and configures our testing environment. before_script: From 0c7f50ff907052e99552e1aa0661e63e378e9bd3 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Fri, 1 Aug 2014 14:28:41 -0400 Subject: [PATCH 402/515] Support for running tests in a few ways --- tests/bootstrap.php | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 1b33b6c629..a28959dcd1 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,25 +1,31 @@ array( basename( dirname( dirname( __FILE__ ) ) ) . '/plugin.php' ), -); +// Support for: +// 1. `WP_DEVELOP_DIR` environment variable +// 2. Plugin installed inside of WordPress.org developer checkout +// 3. Tests checked out to /tmp +if( false !== getenv( 'WP_DEVELOP_DIR' ) ) { + $test_root = getenv( 'WP_DEVELOP_DIR' ); +} else if ( file_exists( '../../../../tests/phpunit/includes/bootstrap.php' ) ) { + $test_root = '../../../..'; +} else if ( file_exists( '/tmp/wordpress-tests-lib/includes/bootstrap.php' ) ) { + $test_root = '/tmp/wordpress-tests-lib'; +} -// If the develop repo location is defined (as WP_DEVELOP_DIR), use that -// location. Otherwise, we'll just assume that this plugin is installed in a -// WordPress develop SVN checkout. +require $test_root . '/includes/functions.php'; -if( false !== getenv( 'WP_DEVELOP_DIR' ) ) { - require getenv( 'WP_DEVELOP_DIR' ) . '/tests/phpunit/includes/bootstrap.php'; -} else { - require '../../../../tests/phpunit/includes/bootstrap.php'; +function _manually_load_plugin() { + require dirname( __FILE__ ) . '/../plugin.php'; } +tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); + +require $test_root . '/includes/bootstrap.php'; // Helper classes -require_once dirname( __FILE__ ) . '/test-json-testcase.php'; +require_once dirname( __FILE__ ) . '/test-json-testcase.php'; \ No newline at end of file From 349123784ff51c5968c3776488e909ec462a5767 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Fri, 1 Aug 2014 14:33:18 -0400 Subject: [PATCH 403/515] Change our plugin activation test --- tests/test-json-plugin.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test-json-plugin.php b/tests/test-json-plugin.php index d63d23c640..3e4bb8fb5f 100644 --- a/tests/test-json-plugin.php +++ b/tests/test-json-plugin.php @@ -15,8 +15,7 @@ class WP_Test_JSON_Plugin extends WP_UnitTestCase { * The plugin should be installed and activated. */ function test_plugin_activated() { - $directory = basename( dirname( dirname( __FILE__ ) ) ); - $this->assertTrue( is_plugin_active( $directory . '/plugin.php' ) ); + $this->assertTrue( class_exists( 'WP_JSON_Posts' ) ); } /** From cd566b58f9e5090be823e48b2ff44b176274b5f1 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Fri, 1 Aug 2014 15:01:05 -0400 Subject: [PATCH 404/515] Improve tests for multisite --- tests/test-json-users.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test-json-users.php b/tests/test-json-users.php index 8bd938c951..17847cb648 100644 --- a/tests/test-json-users.php +++ b/tests/test-json-users.php @@ -18,6 +18,14 @@ public function setUp() { $this->endpoint = new WP_JSON_Users( $this->fake_server ); } + private function allow_user_to_create_users( $user ) { + if ( is_multisite() ) { + update_site_option( 'site_admins', array( $user->user_login ) ); + } else { + $user->set_role( 'administrator' ); + } + } + public function test_get_current_user() { $response = $this->endpoint->get_current_user(); $this->assertNotInstanceOf( 'WP_Error', $response ); @@ -85,7 +93,7 @@ protected function check_get_user_response( $response, $user_obj, $context = 'vi } public function test_create_user() { - $this->user_obj->set_role( 'administrator' ); + $this->allow_user_to_create_users( $this->user_obj ); $data = array( 'username' => 'test_user', 'password' => 'test_password', @@ -112,7 +120,7 @@ public function test_create_user() { } public function test_create_user_missing_params() { - $this->user_obj->set_role( 'administrator' ); + $this->allow_user_to_create_users( $this->user_obj ); $data = array( 'username' => 'test_user', ); @@ -121,7 +129,7 @@ public function test_create_user_missing_params() { } public function test_delete_user() { - $this->user_obj->set_role( 'administrator' ); + $this->allow_user_to_create_users( $this->user_obj ); // Test with a new user, rather than ourselves, to avoid any // complications with doing so. We should check this separately though. @@ -139,7 +147,7 @@ public function test_delete_user() { } public function test_delete_user_reassign() { - $this->user_obj->set_role( 'administrator' ); + $this->allow_user_to_create_users( $this->user_obj ); // Test with a new user, rather than ourselves, to avoid any // complications with doing so. We should check this separately though. From e023cec6663dbed5ee621e6c1519107773ba0853 Mon Sep 17 00:00:00 2001 From: Josh Kadis Date: Fri, 1 Aug 2014 16:36:12 -0400 Subject: [PATCH 405/515] issue 203: test json_last_error() after json_encode() or json_decode() --- lib/class-wp-json-server.php | 68 ++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index ae2b76bc12..5538351275 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -271,6 +271,13 @@ public function serve_request( $path = null ) { $result = json_encode( $this->prepare_response( $result ) ); + // test for json_encode() error + $json_error_message = $this->get_json_last_error(); + if ( $json_error_message ) { + $json_error_obj = new WP_Error( 'json_encode_error', __( 'json_encode() error: ' . $json_error_message ), array( 'status' => 400 ) ); + $result = $this->error_to_response( $json_error_obj ); + } + if ( isset( $_GET['_jsonp'] ) ) { // Prepend '/**/' to mitigate possible JSONP Flash attacks // http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ @@ -376,6 +383,12 @@ public function dispatch() { if ( $supported & self::ACCEPT_JSON ) { $data = json_decode( $this->get_raw_data(), true ); + // test for json_decode() error + $json_error_message = $this->get_json_last_error(); + if ( $json_error_message ) { + return new WP_Error( 'json_decode_error', __( 'json_decode() error: ' . $json_error_message ), array( 'status' => 400 ) ); + } + if ( $data !== null ) { $args = array_merge( $args, array( 'data' => $data ) ); } @@ -413,6 +426,61 @@ public function dispatch() { return new WP_Error( 'json_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) ); } + /** + * Returns if an error occurred during most recent JSON encode/decode + * + * @return boolean|string Boolean false or string error message + */ + protected function get_json_last_error( ) { + // see https://core.trac.wordpress.org/ticket/27799 + if ( ! function_exists( 'json_last_error' ) ) { + return false; + } + + $last_error_code = json_last_error(); + + // just in case JSON_ERROR_NONE is not defined + $error_code_none = defined( 'JSON_ERROR_NONE' ) ? JSON_ERROR_NONE : 0; + + // return false if no error occurred + if ( $last_error_code === $error_code_none ) { + return false; + } + + // use json_last_error_msg() if available + if ( function_exists( 'json_last_error_msg' ) ) { + return json_last_error_msg(); + } + + // otherwise use built-in constants + if ( defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code ) { + return 'Maximum stack depth exceeded'; + } + elseif ( defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code ) { + return 'Invalid or malformed JSON'; + } + elseif ( defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code ) { + return 'Control character error, possibly incorrectly encoded'; + } + elseif ( defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code ) { + return 'Syntax error'; + } + elseif ( defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code ) { + return 'Malformed UTF-8 characters, possibly incorrectly encoded'; + } + elseif ( defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code ) { + return 'One or more recursive references in the value to be encoded'; + } + elseif ( defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code ) { + return 'One or more NAN or INF values in the value to be encoded'; + } + elseif ( defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code ) { + return 'A value of a type that cannot be encoded was given'; + } else { + return 'An unknown error occurred'; + } + } + /** * Sort parameters by order specified in method declaration * From 87c777c8c36af9668dbe53c305dab2f7b1ebf73a Mon Sep 17 00:00:00 2001 From: kellbot Date: Fri, 1 Aug 2014 17:18:21 -0400 Subject: [PATCH 406/515] Updated documentation to reflect routes needing authentication --- docs/routes/routes.md | 103 +++++++++++++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 22 deletions(-) diff --git a/docs/routes/routes.md b/docs/routes/routes.md index 8e89d8e0ce..6f236d7f1d 100644 --- a/docs/routes/routes.md +++ b/docs/routes/routes.md @@ -4,7 +4,9 @@ Posts Create a Post ------------- - POST /posts + POST /posts + + Requires [authentication](http://wp-api.org/guides/authentication.html) ### Input The `data` parameter consists of the elements of the Post object to be @@ -75,13 +77,14 @@ in the Location header. The new Post entity is also returned in the body for convienience. +If the client is not authenticated, a 403 Forbidden response is given. Retrieve Posts -------------- The Posts endpoint returns a Post Collection containing a subset of the site's posts. - GET /posts + GET /posts ### Input #### `filter` @@ -133,7 +136,7 @@ available. Retrieve a Post --------------- - GET /posts/ + GET /posts/ ### Input #### `context` @@ -157,7 +160,9 @@ fields available on the Post depend on the `context` parameter. Edit a Post ----------- - PUT /posts/ + PUT /posts/ + +Requires [authentication](http://wp-api.org/guides/authentication.html) For compatibility reasons, this endpoint also accepts the POST and PATCH methods. Both of these methods have the same behaviour as using PUT. It is @@ -219,11 +224,14 @@ pairs: On a successful update, a 200 OK status is given, indicating the post has been updated. The updated Post entity is returned in the body. +If the client is not authenticated, a 403 Forbidden response is sent. Delete a Post ------------- - DELETE /posts/ + DELETE /posts/ + +Requires [authentication](http://wp-api.org/guides/authentication.html) ### Input #### `force` @@ -242,11 +250,27 @@ later date. If force was set to true, a 200 OK status code will be returned instead, indicating that the post has been permanently deleted. +If the client is not authenticated, a 403 Forbidden status code will be returned. +Retrieve Revisions for a Post +------------------------ + + GET /posts//revisions + +Requires [authentication](http://wp-api.org/guides/authentication.html) + +### Response +If successful, returns a 200 OK status code and revisions for the given post. + +If the client is not authenticated, a 403 Forbidden status code will be returned. + + Create Meta for a Post ------------------------ - POST /posts//meta + POST /posts//meta + +Requires [authentication](http://wp-api.org/guides/authentication.html) ### Input The supplied data should be a Meta object. This data can be submitted via a @@ -266,31 +290,40 @@ specified in the Location header. The new Meta entity is also returned in the body for convienience. +If the client is not authenticated, a 403 Forbidden status code will be returned. Retrieve Meta for a Post ------------------------ - GET /posts//meta + GET /posts//meta +Requires [authentication](http://wp-api.org/guides/authentication.html) + ### Response The response is a Meta entity containing all the post_meta for the specified Post if available. +Returns a 403 Forbidden status code if the client is not authenticated. Retrieve a Meta for a Post ------------------------ - GET /posts//meta/ + GET /posts//meta/ + +Requires [authentication](http://wp-api.org/guides/authentication.html) ### Response The response a Meta entity containing the post_meta for the specified Meta and Post if available. +Returns a 403 Forbidden status code if the client is not authenticated. Edit a Meta for a Post ------------------------ - PUT /posts//meta/ + PUT /posts//meta/ + +Requires [authentication](http://wp-api.org/guides/authentication.html) ### Input The supplied data should be a Meta object. This data can be submitted via a @@ -306,17 +339,20 @@ The `data` parameter should be an array containing the following key value pairs On a successful update, a 200 OK status is given, indicating the post_meta has been updated. The updated Meta entity is returned in the body. +If the client is not authenticated, a 403 Forbidden status code is returned. Delete a Meta for a Post ------------- - DELETE /posts//meta/ + DELETE /posts//meta/ +Requires [authentication](http://wp-api.org/guides/authentication.html) ### Response On successful deletion, a 200 OK status code will be returned, indicating that the post_meta has been permanently deleted. +If the client is not authenticated, a 403 Forbidden status code is returned. Media ===== @@ -327,7 +363,9 @@ Create an Attachment The Create Attachment endpoint is used to create the raw data for an attachment. This is a binary object (blob), such as image data or a video. - POST /media + POST /media + +Requires [authentication](http://wp-api.org/guides/authentication.html) ### Input The attachment creation endpoint can accept data in two forms. @@ -354,6 +392,7 @@ URL specified in the Location header. The new Attachment entity is also returned in the body for convienience. +Returns a 403 Forbidden status code if the client is not authenticated. Get Attachments --------------- @@ -362,7 +401,7 @@ the site's attachments. This endpoint is an extended version of the Post retrieval endpoint. - GET /media + GET /media ### Input #### `fields` @@ -380,7 +419,9 @@ Users Create a User ------------- - POST /users + POST /users + +Requires [authentication](http://wp-api.org/guides/authentication.html) ### Input The supplied data should be a User object. This data can be submitted via a @@ -403,16 +444,20 @@ On a successful creation, a 201 Created status is given, indicating that the user has been created. The user is available canonically from the URL specified in the Location header. -The new User entity is also returned in the body for convienience. +The new User entity is also returned in the body for convenience. +A 403 Forbidden status is returned if the client is not authenticated. Retrieve Users -------------- The Users endpoint returns a User Collection containing a subset of the site's users. - GET /users + GET /users + +Requires [authentication](http://wp-api.org/guides/authentication.html) + ### Input #### `filter` The `filter` parameter controls the query parameters. It is essentially a subset @@ -432,11 +477,15 @@ The parameter should be an array of the following key/value pairs: The response is a User Collection document containing the requested Users if available. +A 403 Forbidden status is returned if the client is not authenticated. + Retrieve a User --------------- - GET /users/ + GET /users/ + +Requires [authentication](http://wp-api.org/guides/authentication.html) ### Input #### `context` @@ -446,7 +495,7 @@ contexts are available: * `view`: The default context. Gives the normal User entity. * `edit`: Context used for extra fields relevant to updating a user. Includes the `extra_capabilities` field; this field contains the capabilities assigned - to the user themselves, rather than those inherited from their roles. + to the user themselves, rather than those inherited from their roles. Requires [authentication](http://wp-api.org/guides/authentication.html). * `embed`: Context used when embedding the response inside another (e.g. post author). This is intended as a minimal subset of the user data to reduce response size. Excludes `roles` and `capabilities`. @@ -457,12 +506,16 @@ Default is "view". (string) The response is a User entity containing the requested User if available. The fields available on the User depend on the `context` parameter. +A 403 Forbidden status is returned if the client is not authenticated. + Retrieve Current User ------------- - GET /users/me + GET /users/me +Requires [authentication](http://wp-api.org/guides/authentication.html) + This endpoint offers a permalink to get the current user, without needing to know the user's ID. @@ -480,14 +533,16 @@ available canonically from the URL specified in the Location header. The User entity containing the current User is also returned in the body for convenience. The fields available on the User depend on the `context` parameter. -If the client is not logged in, a 401 Unauthorized status is given. +If the client is not logged in, a 403 Forbidden status is given. Edit a User ----------- - PUT /users/ + PUT /users/ +Requires [authentication](http://wp-api.org/guides/authentication.html) + For compatibility reasons, this endpoint also accepts the POST and PATCH methods. Both of these methods have the same behaviour as using PUT. It is recommended to use PUT if available to fit with REST convention. @@ -502,12 +557,15 @@ example. On a successful update, a 200 OK status is given, indicating the user has been updated. The updated User entity is returned in the body. +If the client is not logged in, a 403 Forbidden status is given. Delete a User ------------- - DELETE /users/ - + DELETE /users/ + +Requires [authentication](http://wp-api.org/guides/authentication.html) + ### Input #### `force` The `force` parameter controls whether the user is permanently deleted or not. @@ -533,6 +591,7 @@ later date. If force was set to true, a 200 OK status code will be returned instead, indicating that the user has been permanently deleted. +If the client is not authenticated, a 403 Forbidden status is given. Taxonomies ========== From a565fdcd4cfee92afebcf510c917a8829ab12972 Mon Sep 17 00:00:00 2001 From: Mike Auteri Date: Fri, 1 Aug 2014 17:21:21 -0400 Subject: [PATCH 407/515] #347: Added filters to taxonomy based on get_terms args. --- lib/class-wp-json-taxonomies.php | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index c256c6cd77..be9bb34038 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -207,7 +207,7 @@ public function get_terms( $type, $taxonomy ) { * @param string $taxonomy Taxonomy slug * @return array Term collection */ - public function get_taxonomy_terms( $taxonomy ) { + public function get_taxonomy_terms( $taxonomy, $filter = array() ) { if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'json_taxonomy_invalid_id', __( 'Invalid taxonomy ID.' ), array( 'status' => 404 ) ); } @@ -215,7 +215,34 @@ public function get_taxonomy_terms( $taxonomy ) { $args = array( 'hide_empty' => false, ); - + + // Allow args in get_terms function, a common sense list... + $valid_vars = array( + 'orderby', + 'order', + 'exclude', + 'exclude_tree', + 'include', + 'number', + 'fields', + 'slug', + 'parent', + 'hierarchical', + 'child_of', + 'get', + 'name__like', + 'description__like', + 'pad_counts', + 'offset', + 'search', + ); + + foreach ( $valid_vars as $var ) { + if ( isset( $filter[ $var ] ) ) { + $args[ $var ] = apply_filters( 'json_query_var-' . $var, $filter[ $var ] ); + } + } + $terms = get_terms( $taxonomy, $args ); if ( is_wp_error( $terms ) ) { From 69acd57f767782bf1ec089c9eb233741b416e671 Mon Sep 17 00:00:00 2001 From: Josh Pollock Date: Tue, 29 Jul 2014 08:20:06 -0400 Subject: [PATCH 408/515] Made clear that WP.com API was available as part of JetPack Also, added link to codex page for XML-RPC API. --- docs/comparison.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/comparison.md b/docs/comparison.md index 5a8e103215..9c0a661d1c 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -2,8 +2,8 @@ The purpose of this document is to compare the WordPress JSON REST API (WP API) to the other WordPress JSON API projects. Included in this comparison -are the [WordPress.com JSON REST API](http://developer.wordpress.com/docs/api/), -and the built-in XML-RPC API. +are the [WordPress.com JSON REST API](http://developer.wordpress.com/docs/api/) that is availble as part of [JetPack](http://jetpack.me/support/json-api/), +and the [XML-RPC API](http://codex.wordpress.org/XML-RPC_WordPress_API), which is built in to WordPress Core. ## Functionality From a5e651ed41d9c49ce101ed72ede4c8e70f25802a Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 1 Aug 2014 21:47:53 -0500 Subject: [PATCH 409/515] Minor wrapping and grammar fix. --- docs/comparison.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/comparison.md b/docs/comparison.md index 9c0a661d1c..5737d6504b 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -2,8 +2,10 @@ The purpose of this document is to compare the WordPress JSON REST API (WP API) to the other WordPress JSON API projects. Included in this comparison -are the [WordPress.com JSON REST API](http://developer.wordpress.com/docs/api/) that is availble as part of [JetPack](http://jetpack.me/support/json-api/), -and the [XML-RPC API](http://codex.wordpress.org/XML-RPC_WordPress_API), which is built in to WordPress Core. +are the [WordPress.com JSON REST API](http://developer.wordpress.com/docs/api/) +that is availble as part of [JetPack](http://jetpack.me/support/json-api/), and +the [XML-RPC API](http://codex.wordpress.org/XML-RPC_WordPress_API), which is +built into WordPress Core. ## Functionality From d320f371d638de50cf4e8ce44d4246a0c0fef666 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 2 Aug 2014 11:11:12 -0400 Subject: [PATCH 410/515] Check that the JSONP callback is a string first If you pass in an array, this would cause a warning. Props @ircrash --- lib/class-wp-json-server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index ae2b76bc12..e62fac1ded 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -202,7 +202,7 @@ public function serve_request( $path = null ) { } // Check for invalid characters (only alphanumeric allowed) - if ( preg_match( '/\W/', $_GET['_jsonp'] ) ) { + if ( ! is_string( $_GET['_jsonp'] ) || preg_match( '/\W/', $_GET['_jsonp'] ) ) { echo $this->json_error( 'json_callback_invalid', __( 'The JSONP callback function is invalid.' ), 400 ); return false; } From e1cbb89ab4bb9c4eda3ae4225798e24d1cb54393 Mon Sep 17 00:00:00 2001 From: Eddie Hurtig Date: Sun, 3 Aug 2014 13:57:31 -0400 Subject: [PATCH 411/515] Resolves #409. Now matches dashes in addition to alphanumeric and _ characters --- lib/class-wp-json-taxonomies.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index c256c6cd77..ad81db13fd 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -9,28 +9,28 @@ class WP_JSON_Taxonomies { */ public function register_routes( $routes ) { $tax_routes = array( - '/posts/types/(?P\w+)/taxonomies' => array( + '/posts/types/(?P[a-z0-9-_]+)/taxonomies' => array( array( array( $this, 'get_taxonomies_for_type' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), ), - '/posts/types/(?P\w+)/taxonomies/(?P\w+)' => array( + '/posts/types/(?P[a-z0-9-_]+)/taxonomies/(?P[a-z0-9-_]+)' => array( array( array( $this, 'get_taxonomy' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), ), - '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms' => array( + '/posts/types/(?P[a-z0-9-_]+)/taxonomies/(?P[a-z0-9-_]+)/terms' => array( array( array( $this, 'get_terms' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), ), - '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms/(?P\w+)' => array( + '/posts/types/(?P[a-z0-9-_]+)/taxonomies/(?P[a-z0-9-_]+)/terms/(?P[a-z0-9-_]+)' => array( array( array( $this, 'get_term' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), ), '/taxonomies' => array( array( array( $this, 'get_taxonomies' ), WP_JSON_Server::READABLE ), ), - '/taxonomies/(?P\w+)' => array( + '/taxonomies/(?P[a-z0-9-_]+)' => array( array( array( $this, 'get_taxonomy_object' ), WP_JSON_Server::READABLE ), ), - '/taxonomies/(?P\w+)/terms' => array( + '/taxonomies/(?P[a-z0-9-_]+)/terms' => array( array( array( $this, 'get_taxonomy_terms' ), WP_JSON_Server::READABLE ), ), - '/taxonomies/(?P\w+)/terms/(?P\w+)' => array( + '/taxonomies/(?P[a-z0-9-_]+)/terms/(?P[a-z0-9-_]+)' => array( array( array( $this, 'get_taxonomy_term' ), WP_JSON_Server::READABLE ), ), ); From 24c4e526d22fceefcefbbfceea36518d410af0b0 Mon Sep 17 00:00:00 2001 From: Eddie Hurtig Date: Sun, 3 Aug 2014 14:04:19 -0400 Subject: [PATCH 412/515] Refactored regex to use \w again and simply add - to the group --- lib/class-wp-json-taxonomies.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index ad81db13fd..ccbe4b8b4e 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -9,28 +9,28 @@ class WP_JSON_Taxonomies { */ public function register_routes( $routes ) { $tax_routes = array( - '/posts/types/(?P[a-z0-9-_]+)/taxonomies' => array( + '/posts/types/(?P[\w-]+)/taxonomies' => array( array( array( $this, 'get_taxonomies_for_type' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), ), - '/posts/types/(?P[a-z0-9-_]+)/taxonomies/(?P[a-z0-9-_]+)' => array( + '/posts/types/(?P[\w-]+)/taxonomies/(?P[\w-]+)' => array( array( array( $this, 'get_taxonomy' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), ), - '/posts/types/(?P[a-z0-9-_]+)/taxonomies/(?P[a-z0-9-_]+)/terms' => array( + '/posts/types/(?P[\w-]+)/taxonomies/(?P[\w-]+)/terms' => array( array( array( $this, 'get_terms' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), ), - '/posts/types/(?P[a-z0-9-_]+)/taxonomies/(?P[a-z0-9-_]+)/terms/(?P[a-z0-9-_]+)' => array( + '/posts/types/(?P[\w-]+)/taxonomies/(?P[\w-]+)/terms/(?P[\w-]+)' => array( array( array( $this, 'get_term' ), WP_JSON_Server::READABLE | WP_JSON_Server::HIDDEN_ENDPOINT ), ), '/taxonomies' => array( array( array( $this, 'get_taxonomies' ), WP_JSON_Server::READABLE ), ), - '/taxonomies/(?P[a-z0-9-_]+)' => array( + '/taxonomies/(?P[\w-]+)' => array( array( array( $this, 'get_taxonomy_object' ), WP_JSON_Server::READABLE ), ), - '/taxonomies/(?P[a-z0-9-_]+)/terms' => array( + '/taxonomies/(?P[\w-]+)/terms' => array( array( array( $this, 'get_taxonomy_terms' ), WP_JSON_Server::READABLE ), ), - '/taxonomies/(?P[a-z0-9-_]+)/terms/(?P[a-z0-9-_]+)' => array( + '/taxonomies/(?P[\w-]+)/terms/(?P[\w-]+)' => array( array( array( $this, 'get_taxonomy_term' ), WP_JSON_Server::READABLE ), ), ); From 0c23c618dcae6f3a5aaf13aeca2db4c88fd077fb Mon Sep 17 00:00:00 2001 From: Eddie Hurtig Date: Sun, 3 Aug 2014 14:42:39 -0400 Subject: [PATCH 413/515] Updated Tests to check for the updated keys, moved old keys to deprecated list --- tests/test-json-taxonomies.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test-json-taxonomies.php b/tests/test-json-taxonomies.php index 2fcfcb1e75..58fd17e4df 100644 --- a/tests/test-json-taxonomies.php +++ b/tests/test-json-taxonomies.php @@ -25,15 +25,18 @@ public function test_register_routes() { $routes = $this->endpoint->register_routes( $routes ); $this->assertArrayHasKey( '/taxonomies', $routes ); - $this->assertArrayHasKey( '/taxonomies/(?P\w+)', $routes ); - $this->assertArrayHasKey( '/taxonomies/(?P\w+)/terms', $routes ); - $this->assertArrayHasKey( '/taxonomies/(?P\w+)/terms/(?P\w+)', $routes ); + $this->assertArrayHasKey( '/taxonomies/(?P[\w-]+)', $routes ); + $this->assertArrayHasKey( '/taxonomies/(?P[\w-]+)/terms', $routes ); + $this->assertArrayHasKey( '/taxonomies/(?P[\w-]+)/terms/(?P[\w-]+)', $routes ); $deprecated = array( '/posts/types/(?P\w+)/taxonomies', '/posts/types/(?P\w+)/taxonomies/(?P\w+)', '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms', '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms/(?P\w+)', + '/taxonomies/(?P\w+)', + '/taxonomies/(?P\w+)/terms', + '/taxonomies/(?P\w+)/terms/(?P\w+)', ); foreach ( $deprecated as $route ) { From b2e0bdbf75ed3f4bd29190c71cd32ac9efcfb1f9 Mon Sep 17 00:00:00 2001 From: Eddie Hurtig Date: Sun, 3 Aug 2014 14:48:47 -0400 Subject: [PATCH 414/515] Or maybe they shouldn't be listed in deprecated and checked --- tests/test-json-taxonomies.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test-json-taxonomies.php b/tests/test-json-taxonomies.php index 58fd17e4df..547fdc9c2e 100644 --- a/tests/test-json-taxonomies.php +++ b/tests/test-json-taxonomies.php @@ -34,9 +34,6 @@ public function test_register_routes() { '/posts/types/(?P\w+)/taxonomies/(?P\w+)', '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms', '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms/(?P\w+)', - '/taxonomies/(?P\w+)', - '/taxonomies/(?P\w+)/terms', - '/taxonomies/(?P\w+)/terms/(?P\w+)', ); foreach ( $deprecated as $route ) { From e13697ab8d2f62c1c31b35e4e6549d6cf52b27fc Mon Sep 17 00:00:00 2001 From: Eddie Hurtig Date: Sun, 3 Aug 2014 14:54:14 -0400 Subject: [PATCH 415/515] Fixed deprecated keys... to use the newer route --- tests/test-json-taxonomies.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test-json-taxonomies.php b/tests/test-json-taxonomies.php index 547fdc9c2e..879c067ebd 100644 --- a/tests/test-json-taxonomies.php +++ b/tests/test-json-taxonomies.php @@ -30,10 +30,10 @@ public function test_register_routes() { $this->assertArrayHasKey( '/taxonomies/(?P[\w-]+)/terms/(?P[\w-]+)', $routes ); $deprecated = array( - '/posts/types/(?P\w+)/taxonomies', - '/posts/types/(?P\w+)/taxonomies/(?P\w+)', - '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms', - '/posts/types/(?P\w+)/taxonomies/(?P\w+)/terms/(?P\w+)', + '/posts/types/(?P[\w-]+)/taxonomies', + '/posts/types/(?P[\w-]+)/taxonomies/(?P[\w-]+)/taxonomies/(?P[\w-]+)/terms', + '/posts/types/(?P[\w-]+)/taxonomies/(?P[\w-]+)/terms/(?P[\w-]+)', ); foreach ( $deprecated as $route ) { From fc1e07b7e52f93d7fe0294aa1ae3598037239993 Mon Sep 17 00:00:00 2001 From: Eddie Hurtig Date: Sun, 3 Aug 2014 15:00:11 -0400 Subject: [PATCH 416/515] Travis pointed out my typo in the deprecated field --- tests/test-json-taxonomies.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-json-taxonomies.php b/tests/test-json-taxonomies.php index 879c067ebd..e288a98f80 100644 --- a/tests/test-json-taxonomies.php +++ b/tests/test-json-taxonomies.php @@ -31,7 +31,7 @@ public function test_register_routes() { $deprecated = array( '/posts/types/(?P[\w-]+)/taxonomies', - '/posts/types/(?P[\w-]+)/taxonomies/(?P[\w-]+)/taxonomies/(?P[\w-]+)', '/posts/types/(?P[\w-]+)/taxonomies/(?P[\w-]+)/terms', '/posts/types/(?P[\w-]+)/taxonomies/(?P[\w-]+)/terms/(?P[\w-]+)', ); From d8b46789e6d17bfa0583dacd1bc907202d2274ca Mon Sep 17 00:00:00 2001 From: Mike Auteri Date: Sun, 3 Aug 2014 15:22:07 -0400 Subject: [PATCH 417/515] #347: Update to get_terms argument list comment and changed name of taxonomy filter. --- lib/class-wp-json-taxonomies.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index be9bb34038..e95eed3f9a 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -216,7 +216,7 @@ public function get_taxonomy_terms( $taxonomy, $filter = array() ) { 'hide_empty' => false, ); - // Allow args in get_terms function, a common sense list... + // Allow args in get_terms function. This is a partial list and does not include hide_empty and cache_domain. $valid_vars = array( 'orderby', 'order', @@ -239,7 +239,7 @@ public function get_taxonomy_terms( $taxonomy, $filter = array() ) { foreach ( $valid_vars as $var ) { if ( isset( $filter[ $var ] ) ) { - $args[ $var ] = apply_filters( 'json_query_var-' . $var, $filter[ $var ] ); + $args[ $var ] = apply_filters( 'json_tax_query_var-' . $var, $filter[ $var ] ); } } From ca18f0ae61477f0ee7ba0229ad203ff0879d4af6 Mon Sep 17 00:00:00 2001 From: Eddie Hurtig Date: Mon, 4 Aug 2014 00:50:43 -0400 Subject: [PATCH 418/515] Getting Comment with the post ID doesn't work. --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 1cefcf9bed..6ef2d73b07 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1580,7 +1580,7 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen if ( $context === 'single' ) { $parent_fields[] = 'comment'; } - $parent = get_comment( $post['post_parent'] ); + $parent = get_comment( $comment->comment_parent ); $fields['parent'] = $this->prepare_comment( $parent, $parent_fields, 'single-parent' ); } From 2e10792bbb97931798e7cabe315a67ef342946f0 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Mon, 4 Aug 2014 14:26:11 -0400 Subject: [PATCH 419/515] Add newline --- tests/bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a28959dcd1..a17d814398 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -28,4 +28,4 @@ function _manually_load_plugin() { require $test_root . '/includes/bootstrap.php'; // Helper classes -require_once dirname( __FILE__ ) . '/test-json-testcase.php'; \ No newline at end of file +require_once dirname( __FILE__ ) . '/test-json-testcase.php'; From 431d7211bed482db3457d52089b761a1ef0c81ef Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Mon, 4 Aug 2014 14:29:01 -0400 Subject: [PATCH 420/515] Different type, per Ryan's request --- tests/test-json-users.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-json-users.php b/tests/test-json-users.php index 17847cb648..b05a9879d1 100644 --- a/tests/test-json-users.php +++ b/tests/test-json-users.php @@ -18,7 +18,7 @@ public function setUp() { $this->endpoint = new WP_JSON_Users( $this->fake_server ); } - private function allow_user_to_create_users( $user ) { + protected function allow_user_to_create_users( $user ) { if ( is_multisite() ) { update_site_option( 'site_admins', array( $user->user_login ) ); } else { From c1f2097b1894ab6a1fea5776abf7139316d96480 Mon Sep 17 00:00:00 2001 From: "K.Adam White" Date: Mon, 4 Aug 2014 15:41:58 -0400 Subject: [PATCH 421/515] Add steps to readme for executing tests with vagrant ssh -c --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a9b2fa911e..4e0f772453 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ For testing, you'll need a little bit more: vagrant provision ``` -3. Run the testing suite: +3. Log in to the virtual machine and run the testing suite: ```bash vagrant ssh @@ -119,6 +119,13 @@ For testing, you'll need a little bit more: phpunit ``` + You can also execute the tests in the context of the VM without SSHing + into the virtual machine (this is equivalent to the above): + + ```bash + vagrant ssh -c 'cd /vagrant/content/plugins/json-rest-api && phpunit' + ``` + ## Issue Tracking From 7d1df2e3b2048047305968c7e8db6710e3f65139 Mon Sep 17 00:00:00 2001 From: Marko Heijnen Date: Sat, 9 Aug 2014 19:38:22 +0200 Subject: [PATCH 422/515] Fix test_root when using WordPress.org developer checkout. --- tests/bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a17d814398..016e1390f5 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -13,7 +13,7 @@ if( false !== getenv( 'WP_DEVELOP_DIR' ) ) { $test_root = getenv( 'WP_DEVELOP_DIR' ); } else if ( file_exists( '../../../../tests/phpunit/includes/bootstrap.php' ) ) { - $test_root = '../../../..'; + $test_root = '../../../../tests/phpunit'; } else if ( file_exists( '/tmp/wordpress-tests-lib/includes/bootstrap.php' ) ) { $test_root = '/tmp/wordpress-tests-lib'; } From fb682bb474582581d712b5f6ee384f69a8184718 Mon Sep 17 00:00:00 2001 From: Josh Kadis Date: Mon, 11 Aug 2014 16:19:13 -0700 Subject: [PATCH 423/515] move json_encode/decode error translation to get_json_last_error() method --- lib/class-wp-json-server.php | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index de7e4f350b..ef859d083f 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -275,7 +275,7 @@ public function serve_request( $path = null ) { // test for json_encode() error $json_error_message = $this->get_json_last_error(); if ( $json_error_message ) { - $json_error_obj = new WP_Error( 'json_encode_error', __( 'json_encode() error: ' . $json_error_message ), array( 'status' => 400 ) ); + $json_error_obj = new WP_Error( 'json_encode_error', 'json_encode() error: ' . $json_error_message, array( 'status' => 400 ) ); $result = $this->error_to_response( $json_error_obj ); } @@ -387,7 +387,7 @@ public function dispatch() { // test for json_decode() error $json_error_message = $this->get_json_last_error(); if ( $json_error_message ) { - return new WP_Error( 'json_decode_error', __( 'json_decode() error: ' . $json_error_message ), array( 'status' => 400 ) ); + return new WP_Error( 'json_decode_error', 'json_decode() error: ' . $json_error_message, array( 'status' => 400 ) ); } if ( $data !== null ) { @@ -450,35 +450,36 @@ protected function get_json_last_error( ) { // use json_last_error_msg() if available if ( function_exists( 'json_last_error_msg' ) ) { - return json_last_error_msg(); + $error_message = json_last_error_msg(); + return __( sprintf( '%s', $error_message ) ); } // otherwise use built-in constants if ( defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code ) { - return 'Maximum stack depth exceeded'; + return __( 'Maximum stack depth exceeded' ); } elseif ( defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code ) { - return 'Invalid or malformed JSON'; + return __( 'Invalid or malformed JSON' ); } elseif ( defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code ) { - return 'Control character error, possibly incorrectly encoded'; + return __( 'Control character error, possibly incorrectly encoded' ); } elseif ( defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code ) { - return 'Syntax error'; + return __( 'Syntax error' ); } elseif ( defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code ) { - return 'Malformed UTF-8 characters, possibly incorrectly encoded'; + return __( 'Malformed UTF-8 characters, possibly incorrectly encoded' ); } elseif ( defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code ) { - return 'One or more recursive references in the value to be encoded'; + return __( 'One or more recursive references in the value to be encoded' ); } elseif ( defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code ) { - return 'One or more NAN or INF values in the value to be encoded'; + return __( 'One or more NAN or INF values in the value to be encoded' ); } elseif ( defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code ) { - return 'A value of a type that cannot be encoded was given'; + return __( 'A value of a type that cannot be encoded was given' ); } else { - return 'An unknown error occurred'; + return __( 'An unknown error occurred' ); } } From 7d01c96a0840bbc7ff5b5ba98c337dcc76a8f416 Mon Sep 17 00:00:00 2001 From: Josh Kadis Date: Mon, 11 Aug 2014 16:31:39 -0700 Subject: [PATCH 424/515] return JSON encoded error inside serve_request() --- lib/class-wp-json-server.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index ef859d083f..d3b79c77ea 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -277,6 +277,7 @@ public function serve_request( $path = null ) { if ( $json_error_message ) { $json_error_obj = new WP_Error( 'json_encode_error', 'json_encode() error: ' . $json_error_message, array( 'status' => 400 ) ); $result = $this->error_to_response( $json_error_obj ); + $result = json_encode( $result->data[0] ); } if ( isset( $_GET['_jsonp'] ) ) { From 833d32e869bab2cb701604b4a4813a74368da8d1 Mon Sep 17 00:00:00 2001 From: Josh Kadis Date: Thu, 14 Aug 2014 09:57:29 -0700 Subject: [PATCH 425/515] fix status headers and move all translation into WP_JSON_Server::get_json_last_error(), then change to switch() statement for returning --- lib/class-wp-json-server.php | 89 +++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index d3b79c77ea..6803c2db50 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -275,7 +275,7 @@ public function serve_request( $path = null ) { // test for json_encode() error $json_error_message = $this->get_json_last_error(); if ( $json_error_message ) { - $json_error_obj = new WP_Error( 'json_encode_error', 'json_encode() error: ' . $json_error_message, array( 'status' => 400 ) ); + $json_error_obj = new WP_Error( 'json_encode_error', $json_error_message, array( 'status' => 500 ) ); $result = $this->error_to_response( $json_error_obj ); $result = json_encode( $result->data[0] ); } @@ -388,7 +388,7 @@ public function dispatch() { // test for json_decode() error $json_error_message = $this->get_json_last_error(); if ( $json_error_message ) { - return new WP_Error( 'json_decode_error', 'json_decode() error: ' . $json_error_message, array( 'status' => 400 ) ); + return new WP_Error( 'json_decode_error', $json_error_message, array( 'status' => 500 ) ); } if ( $data !== null ) { @@ -430,6 +430,7 @@ public function dispatch() { /** * Returns if an error occurred during most recent JSON encode/decode + * Strings to be translated will be in format like "Encoding error: Maximum stack depth exceeded" * * @return boolean|string Boolean false or string error message */ @@ -443,44 +444,58 @@ protected function get_json_last_error( ) { // just in case JSON_ERROR_NONE is not defined $error_code_none = defined( 'JSON_ERROR_NONE' ) ? JSON_ERROR_NONE : 0; + + // prefix error messages with this string + $error_message_prefix = 'Encoding error:'; + + switch ( true ) { - // return false if no error occurred - if ( $last_error_code === $error_code_none ) { - return false; - } + // return false if no error occurred + case $last_error_code === $error_code_none : + return false; + break; - // use json_last_error_msg() if available - if ( function_exists( 'json_last_error_msg' ) ) { - $error_message = json_last_error_msg(); - return __( sprintf( '%s', $error_message ) ); - } + // use json_last_error_msg() if available + case function_exists( 'json_last_error_msg' ) : + $error_message = json_last_error_msg(); + return __( sprintf( '%s %s', $error_message_prefix, $error_message ) ); + break; - // otherwise use built-in constants - if ( defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code ) { - return __( 'Maximum stack depth exceeded' ); - } - elseif ( defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code ) { - return __( 'Invalid or malformed JSON' ); - } - elseif ( defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code ) { - return __( 'Control character error, possibly incorrectly encoded' ); - } - elseif ( defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code ) { - return __( 'Syntax error' ); - } - elseif ( defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code ) { - return __( 'Malformed UTF-8 characters, possibly incorrectly encoded' ); - } - elseif ( defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code ) { - return __( 'One or more recursive references in the value to be encoded' ); - } - elseif ( defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code ) { - return __( 'One or more NAN or INF values in the value to be encoded' ); - } - elseif ( defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code ) { - return __( 'A value of a type that cannot be encoded was given' ); - } else { - return __( 'An unknown error occurred' ); + // otherwise use built-in constants + case defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code : + return __( sprintf( '%s Maximum stack depth exceeded', $error_message_prefix ) ); + break; + + case defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code : + return __( sprintf( '%s Invalid or malformed JSON', $error_message_prefix ) ); + break; + + case defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code : + return __( sprintf( '%s Control character error, possibly incorrectly encoded', $error_message_prefix ) ); + break; + + case defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code : + return __( sprintf( '%s Syntax error', $error_message_prefix ) ); + break; + + case defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code : + return __( sprintf( '%s Malformed UTF-8 characters, possibly incorrectly encoded', $error_message_prefix ) ); + break; + + case defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code : + return __( sprintf( '%s One or more recursive references in the value to be encoded', $error_message_prefix ) ); + break; + + case defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code : + return __( sprintf( '%s One or more NAN or INF values in the value to be encoded', $error_message_prefix ) ); + break; + + case defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code : + return __( sprintf( '%s A value of a type that cannot be encoded was given', $error_message_prefix ) ); + break; + + default : + return __( sprintf( '%s An unknown error occurred', $error_message_prefix ) ); } } From 6687602b73064d2e8ce6708b7018b2dd02bf2beb Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 6 Aug 2014 11:48:19 -0400 Subject: [PATCH 426/515] Avoid DateTimeZone DateTimeZone can't represent all the valid timezones that we can handle (including manual offsets). Unfortunately, it also doesn't support subclassing DateTimeZone to support it. --- lib/class-wp-json-posts.php | 20 ++++++-------------- plugin.php | 25 +++++++++---------------- tests/test-json-posts.php | 12 ++++-------- 3 files changed, 19 insertions(+), 38 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 1cefcf9bed..211ab82294 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -746,26 +746,20 @@ protected function prepare_post( $post, $context = 'view' ) { if ( $post['post_date_gmt'] === '0000-00-00 00:00:00' ) { $post_fields['date'] = null; - $post_fields_extended['date_tz'] = null; $post_fields_extended['date_gmt'] = null; } else { - $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_date'], $timezone ); - $post_fields['date'] = $date->format( 'c' ); - $post_fields_extended['date_tz'] = $date->format( 'e' ); - $post_fields_extended['date_gmt'] = date( 'c', strtotime( $post['post_date_gmt'] ) ); + $post_fields['date'] = mysql2date( 'c', $post['post_date'], false ); + $post_fields_extended['date_gmt'] = mysql2date( 'c', $post['post_date_gmt'], false ); } if ( $post['post_modified_gmt'] === '0000-00-00 00:00:00' ) { $post_fields['modified'] = null; - $post_fields_extended['modified_tz'] = null; $post_fields_extended['modified_gmt'] = null; } else { - $modified = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_modified'], $timezone ); - $post_fields['modified'] = $modified->format( 'c' ); - $post_fields_extended['modified_tz'] = $modified->format( 'e' ); - $post_fields_extended['modified_gmt'] = date( 'c', strtotime( $post['post_modified_gmt'] ) ); + $post_fields['modified'] = mysql2date( 'c', $post['post_modified'], false ); + $post_fields_extended['modified_gmt'] = mysql2date( 'c', $post['post_modified_gmt'], false ); } // Authorized fields @@ -1603,10 +1597,8 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen // Date $timezone = json_get_timezone(); - $date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $comment->comment_date, $timezone ); - $fields['date'] = $date->format( 'c' ); - $fields['date_tz'] = $date->format( 'e' ); - $fields['date_gmt'] = date( 'c', strtotime( $comment->comment_date_gmt ) ); + $fields['date'] = mysql2date( 'c', $comment->comment_date, false ); + $fields['date_gmt'] = mysql2date( 'c', $comment->comment_date_gmt, false ); // Meta $meta = array( diff --git a/plugin.php b/plugin.php index 04c0be125e..639f62a965 100644 --- a/plugin.php +++ b/plugin.php @@ -493,21 +493,17 @@ function json_ensure_response( $response ) { * @return DateTime DateTime instance. */ function json_parse_date( $date, $force_utc = false ) { - // Default timezone to the server's current one. - $timezone = json_get_timezone(); - if ( $force_utc ) { $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date ); - $timezone = new DateTimeZone( 'UTC' ); } - // Strip millisecond precision (a full stop followed by one or more digits). - if ( strpos( $date, '.' ) !== false ) { - $date = preg_replace( '/\.\d+/', '', $date ); + $regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#'; + + if ( ! preg_match( $regex, $date, $matches ) ) { + return false; } - $datetime = WP_JSON_DateTime::createFromFormat( DateTime::RFC3339, $date, $timezone ); - return $datetime; + return strtotime( $date ); } /** @@ -519,17 +515,14 @@ function json_parse_date( $date, $force_utc = false ) { * null on failure. */ function json_get_date_with_gmt( $date, $force_utc = false ) { - $datetime = json_parse_date( $date, $force_utc ); + $date = json_parse_date( $date, $force_utc ); - if ( empty( $datetime ) ) { + if ( empty( $date ) ) { return null; } - $datetime->setTimezone( json_get_timezone() ); - $local = $datetime->format( 'Y-m-d H:i:s' ); - - $datetime->setTimezone( new DateTimeZone( 'UTC' ) ); - $utc = $datetime->format('Y-m-d H:i:s'); + $utc = date( 'Y-m-d H:i:s', $date ); + $local = get_date_from_gmt( $utc ); return array( $local, $utc ); } diff --git a/tests/test-json-posts.php b/tests/test-json-posts.php index aa0c690002..17f95f55d8 100644 --- a/tests/test-json-posts.php +++ b/tests/test-json-posts.php @@ -79,10 +79,8 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi $this->assertNull( $response_data['date_gmt'] ); } else { - $post_date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_date, $timezone ); - $post_date_gmt = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_date_gmt ); - $this->assertEquals( $post_date->format( 'c' ), $response_data['date'] ); - $this->assertEquals( $post_date_gmt->format( 'c' ), $response_data['date_gmt'] ); + $this->assertEquals( mysql2date( 'c', $post_obj->post_date, false ), $response_data['date'] ); + $this->assertEquals( mysql2date( 'c', $post_obj->post_date_gmt, false ), $response_data['date_gmt'] ); } if ( $post_obj->post_modified_gmt === '0000-00-00 00:00:00' ) { @@ -90,10 +88,8 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi $this->assertNull( $response_data['modified_gmt'] ); } else { - $post_modified = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified, $timezone ); - $post_modified_gmt = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post_obj->post_modified_gmt ); - $this->assertEquals( $post_modified_gmt->format( 'c' ), $response_data['modified_gmt'] ); - $this->assertEquals( $post_modified->format( 'c' ), $response_data['modified'] ); + $this->assertEquals( mysql2date( 'c', $post_obj->post_modified, false ), $response_data['modified'] ); + $this->assertEquals( mysql2date( 'c', $post_obj->post_modified_gmt, false ), $response_data['modified_gmt'] ); } From 6d0ad766ca525c7a3aee921b66554ddf21a5c023 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 6 Aug 2014 12:53:06 -0400 Subject: [PATCH 427/515] Explicitly strip timezones from RFC3339 strings This information is stored on the site, not for each post, so we can't make any assumptions or assertions about what the timezone is. As per RFC3339, the timezone part is optional, so we just strip it instead. --- lib/class-wp-json-posts.php | 19 +++++++------------ plugin.php | 15 +++++++++++++++ tests/test-json-posts.php | 10 ++++------ 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 211ab82294..91e9db1949 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -741,16 +741,13 @@ protected function prepare_post( $post, $context = 'view' ) { ); // Dates - $timezone = json_get_timezone(); - - if ( $post['post_date_gmt'] === '0000-00-00 00:00:00' ) { $post_fields['date'] = null; $post_fields_extended['date_gmt'] = null; } else { - $post_fields['date'] = mysql2date( 'c', $post['post_date'], false ); - $post_fields_extended['date_gmt'] = mysql2date( 'c', $post['post_date_gmt'], false ); + $post_fields['date'] = json_mysql_to_rfc3339( $post['post_date'] ); + $post_fields_extended['date_gmt'] = json_mysql_to_rfc3339( $post['post_date_gmt'] ); } if ( $post['post_modified_gmt'] === '0000-00-00 00:00:00' ) { @@ -758,8 +755,8 @@ protected function prepare_post( $post, $context = 'view' ) { $post_fields_extended['modified_gmt'] = null; } else { - $post_fields['modified'] = mysql2date( 'c', $post['post_modified'], false ); - $post_fields_extended['modified_gmt'] = mysql2date( 'c', $post['post_modified_gmt'], false ); + $post_fields['modified'] = json_mysql_to_rfc3339( $post['post_modified'] ); + $post_fields_extended['modified_gmt'] = json_mysql_to_rfc3339( $post['post_modified_gmt'] ); } // Authorized fields @@ -780,7 +777,7 @@ protected function prepare_post( $post, $context = 'view' ) { if ( empty( $post_fields['format'] ) ) { $post_fields['format'] = 'standard'; } - + if ( 0 === $post['post_parent'] ) { $post_fields['parent'] = null; } @@ -1595,10 +1592,8 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen } // Date - $timezone = json_get_timezone(); - - $fields['date'] = mysql2date( 'c', $comment->comment_date, false ); - $fields['date_gmt'] = mysql2date( 'c', $comment->comment_date_gmt, false ); + $fields['date'] = json_mysql_to_rfc3339( $comment->comment_date ); + $fields['date_gmt'] = json_mysql_to_rfc3339( $comment->comment_date_gmt ); // Meta $meta = array( diff --git a/plugin.php b/plugin.php index 639f62a965..7666b6bdb5 100644 --- a/plugin.php +++ b/plugin.php @@ -527,6 +527,21 @@ function json_get_date_with_gmt( $date, $force_utc = false ) { return array( $local, $utc ); } +/** + * Parses and formats a MySQL datetime (Y-m-d H:i:s) for ISO8601/RFC3339 + * + * Explicitly strips timezones, as datetimes are not saved with any timezone + * information. Including any information on the offset could be misleading. + * + * @param string $date + */ +function json_mysql_to_rfc3339( $date_string ) { + $formatted = mysql2date( 'c', $date_string, false ); + + // Strip timezone information + return preg_replace( '/(?:Z|[+-]\d{2}(?::\d{2})?)$/', '', $formatted ); +} + /** * Retrieve the avatar url for a user who provided a user ID or email address. * diff --git a/tests/test-json-posts.php b/tests/test-json-posts.php index 17f95f55d8..0d93b06fca 100644 --- a/tests/test-json-posts.php +++ b/tests/test-json-posts.php @@ -72,15 +72,13 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi } // Check post dates. - $timezone = json_get_timezone(); - if ( $post_obj->post_date_gmt === '0000-00-00 00:00:00' ) { $this->assertNull( $response_data['date'] ); $this->assertNull( $response_data['date_gmt'] ); } else { - $this->assertEquals( mysql2date( 'c', $post_obj->post_date, false ), $response_data['date'] ); - $this->assertEquals( mysql2date( 'c', $post_obj->post_date_gmt, false ), $response_data['date_gmt'] ); + $this->assertEquals( json_mysql_to_rfc3339( $post_obj->post_date ), $response_data['date'] ); + $this->assertEquals( json_mysql_to_rfc3339( $post_obj->post_date_gmt ), $response_data['date_gmt'] ); } if ( $post_obj->post_modified_gmt === '0000-00-00 00:00:00' ) { @@ -88,8 +86,8 @@ protected function check_get_post_response( $response, $post_obj, $context = 'vi $this->assertNull( $response_data['modified_gmt'] ); } else { - $this->assertEquals( mysql2date( 'c', $post_obj->post_modified, false ), $response_data['modified'] ); - $this->assertEquals( mysql2date( 'c', $post_obj->post_modified_gmt, false ), $response_data['modified_gmt'] ); + $this->assertEquals( json_mysql_to_rfc3339( $post_obj->post_modified ), $response_data['modified'] ); + $this->assertEquals( json_mysql_to_rfc3339( $post_obj->post_modified_gmt ), $response_data['modified_gmt'] ); } From 46498224624f419d915ce1a14afb399dc4d95850 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 6 Aug 2014 12:54:35 -0400 Subject: [PATCH 428/515] Check creating a post with irregular offset Not strictly related to the rest, but double-checks timezone code. --- tests/test-json-posts.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test-json-posts.php b/tests/test-json-posts.php index 0d93b06fca..1c43b58607 100644 --- a/tests/test-json-posts.php +++ b/tests/test-json-posts.php @@ -393,6 +393,21 @@ function test_create_post_custom_date_with_timezone() { $this->assertEquals( $time, strtotime( $new_post->post_date ) ); } + function test_create_post_custom_date_with_irregular_offset() { + $data = $this->set_data(array( + 'date' => '2010-01-01T02:00:00-10:14', + )); + $time = gmmktime( 12, 14, 0, 1, 1, 2010 ); + + $response = $this->endpoint->create_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $new_post = get_post( $response_data['id'] ); + $this->assertEquals( $time, strtotime( $new_post->post_date ) ); + } + function test_create_post_custom_date_gmt() { $data = $this->set_data(array( 'date_gmt' => '2010-01-01T02:00:00Z', From 36916ccb9f2874c9d409424e4c0797d450f6e97a Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sat, 16 Aug 2014 13:03:45 -0400 Subject: [PATCH 429/515] Fix response key to match current master post_id key --- tests/test-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-json-posts.php b/tests/test-json-posts.php index 1c43b58607..5c77719fc4 100644 --- a/tests/test-json-posts.php +++ b/tests/test-json-posts.php @@ -404,7 +404,7 @@ function test_create_post_custom_date_with_irregular_offset() { $this->check_create_response( $response ); $response_data = $response->get_data(); - $new_post = get_post( $response_data['id'] ); + $new_post = get_post( $response_data['ID'] ); $this->assertEquals( $time, strtotime( $new_post->post_date ) ); } From a959ebb4dc84b36d61242ceb950c2738ab5dc55a Mon Sep 17 00:00:00 2001 From: Sarah Gooding Date: Sun, 17 Aug 2014 21:05:36 -0500 Subject: [PATCH 430/515] Docs folder cleanup Added schema to routes docs, removed authentication, comparison, compatibility files along with internals as they are now located in the wp-api.org docs repo. We should not now remove these files from the WP-API plugin folder to eliminate confusion. --- docs/authentication.md | 109 ---- docs/comparison.md | 209 -------- docs/compatibility.md | 109 ---- docs/internals/implementation.md | 52 -- docs/internals/philosophy.md | 72 --- docs/internals/release-process.md | 29 -- docs/routes/routes.md | 826 ++++++++++++++++++++++++++++++ docs/schema.json | 238 --------- docs/schema.md | 825 ----------------------------- 9 files changed, 826 insertions(+), 1643 deletions(-) delete mode 100644 docs/authentication.md delete mode 100644 docs/comparison.md delete mode 100644 docs/compatibility.md delete mode 100644 docs/internals/implementation.md delete mode 100644 docs/internals/philosophy.md delete mode 100644 docs/internals/release-process.md delete mode 100644 docs/schema.json delete mode 100644 docs/schema.md diff --git a/docs/authentication.md b/docs/authentication.md deleted file mode 100644 index c6394fd1e2..0000000000 --- a/docs/authentication.md +++ /dev/null @@ -1,109 +0,0 @@ -Authentication -============== - -There are two options for authenticating with the API. The basic choice boils -down to: - -* Are you a plugin/theme running on the site? Use **cookie authentication** -* Are you a desktop/web/mobile client accessing the site externally? Use - **OAuth authentication** - - -Cookie Authentication ---------------------- -Cookie authentication is the basic authentication method included with -WordPress. When you log in to your dashboard, this sets up the cookies correctly -for you, so plugin and theme developers need only to have a logged-in user. - -However, WP API includes a technique called [nonces][] to avoid [CSRF][] issues. -This prevents other sites from forcing you to perform actions without explicitly -intending to do so. This requires slightly special handling for the API. - -For developers using the built-in Javascript API, this is handled automatically -for you. This is the recommended way to use the API for plugins and themes. -Custom data models can extend `wp.api.models.Base` to ensure this is sent -correctly for any custom requests. - -For developers making manual Ajax requests, the nonce will need to be passed -with each request. The API uses nonces with the action set to `wp_api`. These -can then be passed to the API via the `_wp_json_nonce` data parameter (either -POST data or in the query for GET requests), or via the `X-WP-Nonce` header. - -As an example, this is how the built-in Javascript client creates the nonce: - -```php -wp_localize_script( 'wp-api', 'WP_API_Settings', array( 'root' => esc_url_raw( get_json_url() ), 'nonce' => wp_create_nonce( 'wp_json' ) ) ); -``` - -This is then used in the base model: - -```javascript -options.beforeSend = function(xhr) { - xhr.setRequestHeader('X-WP-Nonce', WP_API_Settings.nonce); - - if (beforeSend) { - return beforeSend.apply(this, arguments); - } -}; -``` - -[nonces]: http://codex.wordpress.org/WordPress_Nonces -[CSRF]: http://en.wikipedia.org/wiki/Cross-site_request_forgery - - -OAuth Authentication --------------------- -OAuth authentication is the main authentication handler used for external -clients. With OAuth authentication, users still only ever log in via the normal -WP login form, and then authorize clients to act on their behalf. Clients are -then issued with OAuth tokens that enable them to access the API. This access -can be revoked by users at any point. - -OAuth authentication uses the [OAuth 1.0a specification][oauth] (published as -RFC5849) and requires installing the [OAuth plugin][oauth-plugin] on the site. -(This plugin will be included with the API when merged into core.) - -Once you have WP API and the OAuth server plugins activated on your server, -you'll need to create a "consumer". This is an identifier for the application, -and includes a "key" and "secret", both needed to link to your site. - -To create the consumer, run the following on your server: -```bash -$ wp oauth1 add - -ID: 4 -Key: sDc51JgH2mFu -Secret: LnUdIsyhPFnURkatekRIAUfYV7nmP4iF3AVxkS5PRHPXxgOW -``` - -This key and secret is your consumer key and secret, and needs to be used -throughout the authorization process. Currently no UI exists to manage this, -however this is planned for a future release. - -For examples on how to use this, both the [CLI client][client-cli] and the -[API console][api-console] make use of the OAuth functionality, and are a great -starting point. - -[oauth]: http://tools.ietf.org/html/rfc5849 -[oauth-plugin]: https://github.com/WP-API/OAuth1 -[client-cli]: https://github.com/WP-API/client-cli -[api-console]: https://github.com/WP-API/api-console - - -Basic Authentication --------------------- -Basic Authentication is an optional authentication handler for external clients. -Due to the complexity of OAuth authentication, Basic authentication can be -useful during development. However, Basic authentication requires passing your -username and password on every request, as well as giving your credentials to -clients, so it is heavily discouraged for production use. - -Basic authentication uses [HTTP Basic Authentication][http-basic] (published as -RFC2617) and requires installing the [Basic Auth plugin][basic-auth-plugin]. - -To use Basic authentication, simply pass the username and password with each -request through the `Authorization` header. This value should be encoded as per -the HTTP Basic specification. - -[http-basic]: https://tools.ietf.org/html/rfc2617 -[basic-auth-plugin]: https://github.com/WP-API/Basic-Auth diff --git a/docs/comparison.md b/docs/comparison.md deleted file mode 100644 index 5737d6504b..0000000000 --- a/docs/comparison.md +++ /dev/null @@ -1,209 +0,0 @@ -# WordPress JSON API Comparison - -The purpose of this document is to compare the WordPress JSON REST API (WP -API) to the other WordPress JSON API projects. Included in this comparison -are the [WordPress.com JSON REST API](http://developer.wordpress.com/docs/api/) -that is availble as part of [JetPack](http://jetpack.me/support/json-api/), and -the [XML-RPC API](http://codex.wordpress.org/XML-RPC_WordPress_API), which is -built into WordPress Core. - -## Functionality - -### Key -* **M**: Multisite only -* **P**: Available via (official) plugin -* **~**: Partially available - -### Authentication - -| Feature | WP API | WP.com JSON API | XML-RPC API | -|:-------------------------|:-------|:----------------|:-----------:| -| Cookie-based | X | | | -| Basic authentication | P | | X | -| OAuth1 | X | | | -| OAuth2 | | X | | -| Custom authentication | X | | ~ | - -Custom authentication can be used with WP API via the `determine_current_user` -filter. With the XML-RPC API, only one custom handler can be added at a time via -subclassing. - -### General - -| Feature | WP API | WP.com JSON API | XML-RPC API | -|:-------------------------|:-------|:----------------|:-----------:| -| Add custom routes | X | | X | -| Modify responses | X | | X | -| Support for other formats| X | | | -| Internally usable | X | | | -| Plugin/theme support | X | ~ | | -| Usable while offline | X | | X | - -WP API supports fully replacing JSON responses with an alternative format, such -as MsgPack, as well as replacing the HTTP layers with others. The API can also -be called internally as normal functions, without causing side effects (HTTP -headers, premature exit, etc). - -WP API supports use in the browser by themes/plugins simply by enqueuing the -JS client. WP.com API requires writing custom code to communicate with the API. - - -### Site - -| Feature | WP API | WP.com JSON API | XML-RPC API | -|:-------------------------|:-------|:----------------|:-----------:| -| Basic Site Information | X | X | M | -| Retrieve options data | | X | ~ | -| Update options data | | | ~ | -| Retrieve post count | X | X | | -| List available routes | X | X | X | - -The XML-RPC API only exposes a limited subset of the available options in -WordPress. - -### Users - -| Feature | WP API | WP.com JSON API | XML-RPC API | -|:-------------------------|:-------|:----------------|:-----------:| -| Create a user | X | | | -| List all users | X | X | X | -| Retrieve a user | X | | X | -| Retrieve current user | X | X | X | -| Edit a user | X | | X | -| Delete a user | X | | | - -### Posts - -| Feature | WP API | WP.com JSON API | XML-RPC API | -|:-------------------------|:-------|:----------------|:-----------:| -| Create a post | X | X | X | -| List all posts | X | X | X | -| Retrieve a post | X | X | X | -| Retrieve a post by slug | ~ | X | | -| Edit a post | X | X | X | -| Delete a post | X | X | X | -| Create meta for a post | X | X | X | -| Retrieve meta for a post | X | X | X | -| Edit meta for a post | X | X | X | -| Delete meta for a post | X | X | X | - -WP API does not provide a specialised page-by-slug retrieval method, but allows -accessing via WP Query: - - GET /wp-json/posts?filter[name]=my-post-slug - -### Comments - -| Feature | WP API | WP.com JSON API | XML-RPC API | -|:-------------------------|:-------|:----------------|:-----------:| -| Create a comment | | X | X | -| Create a reply | | X | X | -| List all comments | | X | X | -| List comments for a post | X | X | X | -| Retrieve a comment | X | X | X | -| Edit a comment | | X | X | -| Delete a comment | X | X | X | - -### Post-Related Data - -| Feature | WP API | WP.com JSON API | XML-RPC API | -|:-------------------------|:-------|:----------------|:-----------:| -| List all post types | X | | X | -| Retrieve post type | X | | X | -| List all post statuses | X | | X | -| Retrieve post status | | | | - -### Taxonomies - -| Feature | WP API | WP.com JSON API | XML-RPC API | -|:-------------------------|:-------|:---------------:|:-----------:| -| Create a term | | X | X | -| List all terms | X | X | X | -| Retrieve a term | X | X | X | -| Edit a term | | X | X | -| Delete a term | | X | X | -| List all taxonomies | X | | X | -| Retrieve a taxonomy | X | | X | - -Taxonomies are defined in code, so no APIs can create, modify, or delete -taxonomies. - -### Revisions - -| Feature | WP API | WP.com JSON API | XML-RPC API | -|:-------------------------|:-------|:---------------:|:-----------:| -| Get revisions for a post | X | | X | -| Restore revision | | | X | - -### Media - -| Feature | WP API | WP.com JSON API | XML-RPC API | -|:-------------------------|:-------|:---------------:|:-----------:| -| Create an attachment | X | X | X | -| List all attachments | X | X | X | -| Retrieve an attachment | X | X | X | -| Edit an attachment | X | X | X | -| Delete an attachment | X | X | X | - -### Pages - -| Feature | WP API | WP.com JSON API | XML-RPC API | -|:-------------------------|:-------|:---------------:|:-----------:| -| Create a page | X | X | X | -| List all pages | X | X | X | -| Retrieve a page | X | X | X | -| Retrieve a page by slug | X | X | | -| Edit a page | X | X | X | -| Delete a page | X | X | X | - -## Formats - -### Posts - -See [read format](http://wp-api.github.io/comparison/post.html) vs -[edit format](http://wp-api.github.io/comparison/post-edit.html). The edit -format is triggered in both by appending `?context=edit` to the resource. - -**Key differences:** - -* WP.com API changes the format of the `content`/`excerpt` fields if in edit - mode. WP API separates the fields into `content`/`content_raw` and - `excerpt`/`excerpt_raw` respectively. - - With combined fields, clients cannot display the existing content and also - allow editing without sending multiple requests. - -* WP API links to all resources related to the current one, including the - author endpoint and the related collection (e.g. `/posts` or `/media`). WP.com - API contains a limited subset of links, and does not include author or - collection links. - -* WP.com API embeds all attachments in the post. WP API does not currently do - this. Attachments belonging to the post can still be queried via - `/media?filter[post_parent]=`. - -## Media - -See [read format](http://wp-api.github.io/comparison/media.html) vs -[edit format](http://wp-api.github.io/comparison/media-edit.html). The edit -format is triggered in both by appending `?context=edit` to the resource. - -**Key differences:** - -* WP API does not expose the caption. This is a bug, and will be fixed in 1.1. - -* WP API treats media as a full custom post type, and includes all post-related - data. WP.com API treats media as a different type, and only gives a limited - subset of all data back. - -* WP.com API does not expose intermediate size URLs, as Photon is used for - WP.com and Jetpack image resizing. WP API exposes all intermediate size URLs - where possible. - -* WP.com API does not link to any related resources. WP API links to all related - resources, such as the parent, the collection and the author. - -* WP.com API does not expose any authorship data, instead relying on clients to - query for the post parent, and work out the authorship of the parent post. - WP API treats this as normal post data, with its own author, and embeds the - entire user object for the view context (as with normal posts). diff --git a/docs/compatibility.md b/docs/compatibility.md deleted file mode 100644 index 00fedbe403..0000000000 --- a/docs/compatibility.md +++ /dev/null @@ -1,109 +0,0 @@ -Notes -===== - -### Inputting data as an "array" -Endpoints may allow passing in an array of data, typically used for querying -entities. URL semantics do not specify how to pass this, however the convention -used by PHP and this API is to pass them with the array name concatenated with -the key name in square brackets. - -For example, a map of key to value (associative arrays) would look like: - - filter[post_status]=draft&filter[s]=foo - -This is equivalent to the following PHP representation: - -```php -$filter = array( - 'post_status' => 'draft', - 's' => 'foo' -); -``` - -Or in Javascript: - -```js -var filter = { - 'post_status': 'draft', - 's': 'foo', -}; -``` - -For lists (numeric arrays), the same syntax is used but with empty key values: - - type[]=post&type[]=page - -This is equivalent to the following PHP representation: - -```php -$type = array( - 'post', - 'page' -); -``` - -Or in Javascript: - -```js -var type = [ - 'post', - 'page' -]; -``` - -These can be combined; for example, using `post__in`: - - filter[post__in][]=1&filter[post__in][]=4&filter[post__in][]=5 - -This is equivalent to the following PHP representation: - -```php -$filter = array( - 'post__in' => array( - 1, - 4, - 5 - ) -); -``` - -Or in Javascript: - -```js -var filter = { - 'post__in': [ - 1, - 4, - 5 - ] -} -``` - - -### JSON data input -Some posts allow directly passing JSON data (usually an entity) via the request -body. These should be specified with a Content-Type header of `application/json` -although individual endpoints may prefer more specific types. - -If your client platform does not support native JSON encoding, the data can be -submitted via a regular HTTP multipart body, with properties set as values to -the `data` parameter. - -That is, the following are equivalent: - -Content-Type: application/x-www-form-urlencoded - - data[title]=Hello%20World!&data[content_raw]=Content&data[excerpt_raw]=Excerpt - - -Content-Type: application/json - - {"title":"Hello World!","content_raw":"Content","excerpt_raw":"Excerpt"} - - -### HTTP method compatibility -Due to their relatively new nature, some methods such as PATCH may not be -supported by client software. To emulate support for this, a `_method` parameter -may be passed via the URL with the value set to a valid HTTP method (DELETE, -GET, HEAD, PATCH, POST, PUT, DELETE). Note that this must be passed via the URL -and cannot be passed in the HTTP body. diff --git a/docs/internals/implementation.md b/docs/internals/implementation.md deleted file mode 100644 index 2dea658406..0000000000 --- a/docs/internals/implementation.md +++ /dev/null @@ -1,52 +0,0 @@ -Implementation Details -====================== - -Method Naming -------------- -By convention, all functions and methods use underscore_case, as per the -[WordPress Coding Standards](http://make.wordpress.org/core/handbook/coding-standards/php/). - - -Routing -------- -The routing code is written in such a way to ensure that the input and output -are handled only by this code. - -The majority of state is handled in the `serve_request()` method, which handles -both reading input and serializing the output to JSON. This is responsible for -translating HTTP to an internal representation suitable for dispatching. - -The routing code also operates with global state, as it's intended to handle the -HTTP interactions, which is inherently global in PHP. - -The `dispatch()` method handles matching the requested route with an endpoint. -This method also deals with global state for GET and POST parameters, however -this is not the optimal situation, as it requires reimplementation of the -routing code when embedding the API. - -This could possibly be optimized by performing straight string matching for the -static portion of the URL. Symfony uses this internally in their routing, -although they don't allow arbitrary regular expressions for routes. - - -Parameters ----------- -Endpoints take parameters as direct function arguments. This fits in with the -philosophy that the endpoints should match up with functions fairly directly. -This means that in order to write an endpoint, all you need to know is how to -write a function. - -This uses the Reflection API to match provided parameters up with the function's -parameters in the correct order, and with default values as needed. The -performance of this API is anecdotally not a real consideration, however -benchmarks are yet to be taken in a rigorous manner. - -Intentionally, it is not possible to get all supplied parameters. This is -regarded as a bad API smell, as each parameter should be fully documented for -consumers. It is possible to construct hacks around this using the -`json_dispatch_args` filter, but this is intentionally made to feel hacky. - -Some parameters give information about the context of the call. These are -prefixed with an underscore, and are *always* set. This ensures that a rogue -consumer can't pass in the internal name to override it and possibly cause a -security issue. diff --git a/docs/internals/philosophy.md b/docs/internals/philosophy.md deleted file mode 100644 index d5ff7ae09f..0000000000 --- a/docs/internals/philosophy.md +++ /dev/null @@ -1,72 +0,0 @@ -Philosophy -========== - -Rules ------ - -### Rule 1: Avoid writing global state - - For example, the routing code is responsible for handling all input and output, - so direct calls from methods to functions such as `header()` or `echo` should - be avoided completely. - -### Rule 2: Endpoints must be embeddable - - Endpoints should be able to call each other without worrying about global state. - This follows from Rule 1, in that global state must be avoided for this, however - this also means that endpoints can be expected to be called internally. - -### Rule 3: Explict is better than implicit - - You should never have to guess where a variable came from, or why a method was - called. Routes should have a 1:1 mapping with endpoints and magic catch-all - routes should be avoided. Parameters should always be documented as if the API - was a closed (source) box. - -### Rule 4: Reduce, reuse, recycle - - Reduce the complexity of clients and the server. - - Reuse code where possible. - - Recycle endpoints by building on the existing instead of reinventing. - - -Guidelines ----------- - -### Guideline 1: Endpoints should operate like regular functions - - By using the API, you're essentially using a form of Remote Procedure Call. - Both remote and local calls should look essentially the same while operating - within the convention of REST calls for the remote calls. - - Parameters to endpoints follow this rule, wherein remote parameters are - matched up with defined parameters in the endpoint. More detailed structures - are handled as entities, which have a clearly defined structure in - the specification. - - This follows from Rules 2 and 3. - -### Guideline 2: Endpoints should avoid assuming HTTP/JSON characteristics - - Although the API usually operates over HTTP using JSON, endpoints can safely - assume that the serialization may take place using a different serializer - (such as protobufs or bson) over a different transport (such as ZeroMQ). - - This doesn't mean that endpoints need to reinvent the wheel; HTTP status codes - and header names are used since this is the normal state. However, values - should never be encoded in endpoints for HTTP transport rules or JSON encoding - rules. - - This follows from Rules 1 and 2. - -### Guideline 3: Base responses around entities - - Being explicit with data formats means documenting everything. No one likes - writing documentation constantly, so these data formats should be based around - the concept of reusable entities. - - This simplifies the code and reduces the workload of documentation. - - This follows from Rules 3 and 4. diff --git a/docs/internals/release-process.md b/docs/internals/release-process.md deleted file mode 100644 index b75f72716e..0000000000 --- a/docs/internals/release-process.md +++ /dev/null @@ -1,29 +0,0 @@ -Release Process -=============== -Here's the process used to generate new releases for the API plugin. - -* Write changelog items for this release. Generate these automatically with the - following script: - - SINCE=$1 - if [[ -z $SINCE ]]; then - echo "usage: changelog " - exit 1 - fi - - OUTPUT=$(git log $SINCE... --reverse --format="format:- %w(80,0,4)%B") - OUTPUT=$(echo "$OUTPUT" | awk -v nlines=2 "/^ *git-svn-id:/ {for (i=0; i ; DQUOTE must be escaped with "\" + +Note that as per ABNF, literal strings are case insensitive. That is: + + example-field = "id" + example-field = "ID" + +Providers SHOULD use the capitalisation as per this specification to ensure +maximum compatibility with consumers. Consumers SHOULD ignore the case of +literal strings when parsing data. + +[RFC5234]: http://tools.ietf.org/html/rfc5234 + + +Entities +======== + +Index +----- +The Index entity is a JSON object with site properties. The following properties +are defined for the Index entity object. + +### `name` +The `name` field is a string with the site's name. + +### `description` +The `description` field is a string with the site's description. + +### `URL` +The `URL` field is a string with the URL to the site itself. + +### `routes` +The `routes` field is an object with keys as a route and the values as a route +descriptor. + +The route is a string giving the URL template for the route, relative to the API +root. The template contains URL parts separated by forward slashes, with each +URL part either a static string, or a route variable encased in angle brackets. + + route = ( "/" + / *( "/" ( token / route-variable ) ) ) + route-variable = "<" token ">" + +These routes can be converted into URLs by replacing all route variables with +their relevant values, then concatenating the relative URL to the API base. + +The route descriptor is an object with the following defined properties. + +* `supports`: A JSON array of supported HTTP methods (verbs). Possible values + are "HEAD", "GET", "POST", "PUT", "PATCH", "DELETE" +* `accepts_json`: A boolean indicating whether data can be passed directly via a + POST request body. Default for missing properties is false. +* `meta`: An Entity Meta entity. Typical `links` values consist of a `self` link + pointing to the route's full URL. + +### `meta` +The `meta` field is a Entity Meta entity with metadata relating to the entity +representation. + +Typical `links` values for the meta object consist of a `help` key with the +value indicating a human-readable documentation page about the API. + +### Example + + { + "name": "My WordPress Site", + "description": "Just another WordPress site", + "URL": "http:\/\/example.com", + "routes": { + "\/": { + "supports": [ + "HEAD", + "GET" + ], + "meta": { + "self": "http:\/\/example.com\/wp-json\/" + } + }, + "\/posts": { + "supports": [ + "HEAD", + "GET", + "POST" + ], + "meta": { + "self": "http:\/\/example.com\/wp-json\/posts" + }, + "accepts_json": true + }, + "\/posts\/": { + "supports": [ + "HEAD", + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "accepts_json": true + }, + "\/posts\/\/revisions": { + "supports": [ + "HEAD", + "GET" + ] + }, + "\/posts\/\/comments": { + "supports": [ + "HEAD", + "GET", + "POST" + ], + "accepts_json": true + }, + "\/posts\/\/comments\/": { + "supports": [ + "HEAD", + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "accepts_json": true + }, + }, + "meta": { + "links": { + "help": "https:\/\/github.com\/WP-API\/WP-API", + "profile": "https:\/\/raw.github.com\/WP-API\/WP-API\/master\/docs\/schema.json" + } + } + } + +Post +---- +The Post entity is a JSON object of post properties. Unless otherwise defined, +properties are available in all contexts. The following properties are defined +for the Post entity object: + +### `title` +The `title` field is a string with the post's title. + +### `date`, `date_gmt` +The `date` and `date_gmt` fields are strings with the post's creation date and +time in the local time and UTC respectively. These fields follow the [RFC3339][] +Section 5.6 datetime representation. + + date = date-time + date_gmt = date-time + +[RFC3339]: http://tools.ietf.org/html/rfc3339 + +### `modified`, `modified_gmt` +The `modified` and `modified_gmt` fields are strings with the post's last +modification date and time in the local time and UTC respectively. These fields +follow the [RFC3339][] Section 5.6 datetime representation. + + modified = date-time + modified_gmt = date-time + +### `date_tz`, `modified_tz` +The `date_tz` and `modified_tz` fields are strings with the timezone applying to +the `date` and `modified` fields respectively. The timezone is a [Olsen zoneinfo +database][] identifier. While the `date` and `modified` fields include timezone +offset information, the `date_tz` and `modified_tz` fields allow proper data +operations across Daylight Savings Time boundaries. + +Note that in addition to the normal Olsen timezones, manual offsets may be +given. These manual offsets use the deprecated `Etc/GMT+...` zones and specify +an integer offset in hours from UTC. + + timezone = Olsen-timezone / manual-offset + manual-offset = "Etc/GMT" ("-" / "+") 1*2( DIGIT ) + +Consumers SHOULD use the fields if they perform mathematical operations on the +`date` and `modified` fields (such as adding an hour to the last modification +date) rather than relying on the `time-offset` in the `date` or +`modified` fields. + +[Olsen zoneinfo database]: https://en.wikipedia.org/wiki/Tz_database + +### `status` +The `status` field is a string with the post's status. This status relates to +where the post is in the editorial process. These are usually set values, but +some providers may have extra post statuses. + + post-status = "draft" / "pending" / "private" / "publish" / "trash" / token + +Consumers who encounter an unknown or missing post status SHOULD treat it the +same as a "draft" status. + +### `type` +The `type` field is a string with the post's type. This field is specific to +providers, with the most basic representation being "post". The type of the +post usually relates to the fields in the Post entity, with other types having +additional fields specific to the type. + + post-type = "post" / token + +Consumers who encounter an unknown or missing post type SHOULD treat it the same +as a "post" type. + +### `name` +The `name` field is a string with the post's slug. + +### `author` +The `author` field is a User entity with the user who created the post. + +### `password` +The `password` field is a string with the post's password. A zero-length +password indicates that the post does not have a password. + +Consumers who encounter a missing password MUST treat it the same as a +zero-length password. + +### `content` +The `content` field is a string with the post's content. + +### `excerpt` +The `excerpt` field is a string with the post's excerpt. This is usually a +shortened version of the post content, suitable for displaying in +collection views. + +Consumers who encounter a missing excerpt MAY present a shortened version of the +`content` field instead. + +### `content_raw`, `excerpt_raw` +The `content_raw` and `excerpt_raw` fields are strings with the post's content +and excerpt respectively. Unlike the `content` and `excerpt` fields, the value +has not been passed through internal filtering, and is suitable for editing. + +(Context Availability: `edit`) + +### `parent` +The `parent` field is an integer or JSON object with the post's parent +post ID. A literal zero indicates that the post does not have a parent +post. + + post-parent = "0" / 1*DIGIT + +Consumers who encounter a missing parent ID MUST treat it the same as a parent +post ID of 0. + +Parent fields will be expanded into a full Post entity in the `view` or `edit` +contexts, but only one level deep. The embedded Post entity will be rendered +using the `parent` context. + +In the `parent` context, the field will contain an integer with the post's +parent post ID as above. + +### `link` +The `link` field is a string with the full URL to the post's canonical view. +This is typically the human-readable location of the entity. + +### `guid` +The `guid` field is a string with the post's globally unique identifier (GUID). + +The GUID is typically in URL form, as this is a relatively easy way of ensuring +that the GUID is globally unique. However, consumers MUST NOT treat the GUID as +a URL, and MUST treat the GUID as a string of arbitrary characters. + +### `menu_order` +The `menu_order` field is an integer with the post's sorting position. This is +typically used to affect sorting when displaying the post in menus or lists. +Larger integers should be treated as sorting before smaller integers. + + menu-order = 1*DIGIT / "-" 1*DIGIT + +Consumers who encounter a missing sorting position MUST treat it the same as a +sorting position of 0. + +### `comment_status` +The `comment_status` field is a string with the post's current commenting +status. This field indicates whether users can submit comments to the post. + + post-comment-status = "open" / "closed" / token + +Providers MAY use statuses other than "open" or "closed" to indicate other +statuses. Consumers who encounter an unknown or missing comment status SHOULD +treat it as "closed". + +### `ping_status` +The `ping_status` field is a string with the post's current pingback/trackback +status. This field indicates whether users can submit pingbacks or trackbacks +to the post. + + ping-status = "open" / "closed" / token + +Providers MAY use statuses other than "open" or "closed" to indicate other +statuses. Consumers who encounter an unknown or missing ping status SHOULD treat +it as "closed". + +### `sticky` +The `sticky` field is a boolean indicating whether the post is marked as a +sticky post. Consumers typically display sticky posts before other posts in +collection views. + +### `post_thumbnail` +The `post_thumbnail` field is a Media entity. + +### `post_format` +The `post_format` field is a string with the post format. The post format +indicates how some meta fields should be displayed. For example, posts with the +"link" format may wish to display an extra link to a URL specified in a meta +field or emphasise a link in the post content. + + post-format = "standard" / "aside" / "gallery" / "image" / "link" / "status" / "quote" / "video" / "audio" / "chat" + +Providers MUST NOT use post formats not specified by this specification, unless +specified in a subsequent version of the specification. Consumers MUST treat +unknown post formats as "standard". + +### `terms` +The `terms` field is a Term collection. + +### `post_meta` +The `meta` field is a Metadata entity with metadata relating to the post. + +### `meta` +The `meta` field is a Entity Meta entity with metadata relating to the entity +representation. + +### Example + + { + "ID": 1, + "title": "Hello world!q", + "status": "publish", + "type": "post", + "author": { + "ID": 1, + "name": "admin", + "slug": "admin", + "URL": "", + "avatar": "http:\/\/0.gravatar.com\/avatar\/c57c8945079831fa3c19caef02e44614&d=404&r=G", + "meta": { + "links": { + "self": "http:\/\/example.com\/wp-json\/users\/1", + "archives": "http:\/\/example.com\/wp-json\/users\/1\/posts" + } + }, + "first_name": "", + "last_name": "" + }, + "content": "

Welcome to WordPress. This is your first post. Edit or delete it, then start blogging!<\/p>\n", + "parent": 0, + "link": "http:\/\/example.com\/2013\/06\/02\/hello-world\/", + "date": "2013-06-02T05:28:00+10:00", + "modified": "2013-06-30T13:56:57+10:00", + "format": "standard", + "slug": "hello-world", + "guid": "http:\/\/example.com\/?p=1", + "excerpt": "", + "menu_order": 0, + "comment_status": "open", + "ping_status": "open", + "sticky": false, + "date_tz": "Australia\/Brisbane", + "date_gmt": "2013-06-02T05:28:00+00:00", + "modified_tz": "Australia\/Brisbane", + "modified_gmt": "2013-06-30T03:56:57+00:00", + "password": "", + "post_meta": [ + ], + "meta": { + "links": { + "self": "http:\/\/example.com\/wp-json\/posts\/1", + "author": "http:\/\/example.com\/wp-json\/users\/1", + "collection": "http:\/\/example.com\/wp-json\/posts", + "replies": "http:\/\/example.com\/wp-json\/posts\/1\/comments", + "version-history": "http:\/\/example.com\/wp-json\/posts\/1\/revisions" + } + }, + "featured_image": null, + "terms": { + "category": { + "ID": 1, + "name": "Uncategorized", + "slug": "uncategorized", + "parent": null, + "count": 7, + "meta": { + "links": { + "collection": "http:\/\/example.com\/wp-json\/taxonomies\/category\/terms", + "self": "http:\/\/example.com\/wp-json\/taxonomies\/category\/terms\/1" + } + } + } + } + } + + + +Entity Meta +----------- +The Entity Meta entity is a JSON object with custom metadata relating to the +representation of the parent entity. + +The following properties are defined for the Entity Meta entity object: + +### `links` +The `links` field is a JSON object with hyperlinks to related entities. Each +item's key is a link relation as per the [IANA Link Relations registry][] with +the value of the item being the corresponding link URL. + +Typical link relations are: + +* `self`: A URL pointing to the current entity's location. +* `up`: A URL pointing to the parent entity's location. +* `collection`: A URL pointing to a collection that the entity is a member of. + +[IANA Link Relations registry]: http://www.iana.org/assignments/link-relations/link-relations.xml + + +User +---- +The User entity is a JSON object with user properties. The following properties +are defined for the User entity object: + +### `ID` +The `ID` field is an integer with the user's ID. + +### `name` +The `name` field is a string with the user's display name. + +### `slug` +The `slug` field is a string with the user's slug. + +### `URL` +The `URL` field is a string with the URL to the author's site. This is typically +an external link of the author's choice. + +### `avatar` +The `avatar` field is a string with the URL to the author's avatar image. + +Providers SHOULD ensure that for users without an avatar image, this field is +either zero-length or the URL returns a HTTP 404 error code on access. Consumers +MAY display a default avatar instead of a zero-length or URL which returns +a HTTP 404 error code. + +### `meta` +The `meta` field is a Entity Meta entity with metadata relating to the entity +representation. + + +Metadata +-------- +The Metadata entity is a JSON array with metadata fields. Each metadata field is +a JSON object with `id`, `key` and `value` fields. + +### `id` +The `id` field of the metadata field is a positive integer with the internal +metadata ID. + +### `key` +The `key` field of the metadata field is a string with the metadata field name. + +### `value` +The `value` field of the metadata field is a string with the metadata +field value. + + +Comment +------- +The Comment entity is a JSON object with comment properties. The following +properties are defined for the Comment entity object: + +### `ID` +The `ID` field is an integer with the comment's ID. + +### `content` +The `content` field is a string with the comment's content. + +### `status` +The `status` field is a string with the comment's status. This field indicates +whether the comment is in the publishing process, or if it has been deleted or +marked as spam. + + comment-status = "hold" / "approved" / "spam" / "trash" / token + +Providers MAY use other values to indicate other statuses. Consumers who +encounter an unknown or missing status SHOULD treat it as "hold". + +### `type` +The `type` field is a string with the comment's type. This is usually one of the +following, but providers may provide additional values. + + comment-type = "comment" / "trackback" / "pingback" / token + +Providers MAY use other values to indicate other types. Consumers who encounter +an unknown or missing status SHOULD treat it as "comment". + +### `post` +The `post` field is an integer with the parent post for the comment, or a Post +entity describing the parent post. A literal zero indicates that the comment +does not have a parent post. + + comment-post-parent = "0" / 1*DIGIT + +Consumers who encounter a missing post ID MUST treat it the same as a parent +post ID of 0. + +### `parent` +The `post` field is an integer with the parent comment, or a Comment entity +describing the parent comment. A literal zero indicates that the comment does +not have a parent comment. + + comment-parent = "0" / 1*DIGIT + +Consumers who encounter a missing parent ID MUST treat it the same as a parent +comment ID of 0. + +### `author` +The `author` field is a User entity with the comment author's data, or a +User-like object for anonymous authors. The User-like object contains the +following properties: + +#### `ID` +The `ID` property on the User-like object is always set to `0` for anonymous +authors. + +#### `name` +The `name` property on the User-like object is a string with the author's name. + +#### `URL` +The `URL` property on the User-like object is a string with the author's URL. + +#### `avatar` +The `avatar` property on the User-like object is a string with the URL to the +author's avatar image. + +This property should be treated the same as the avatar property on the +User entity. + + +### `date`, `date_gmt` +The `date` and `date_gmt` fields are strings with the post's creation date and +time in the local time and UTC respectively. These fields follow the [RFC3339][] +Section 5.6 datetime representation. + + date = date-time + date_gmt = date-time + +This field should be treated the same as the `date` and `date_gmt` properties on +a Post entity. + +[RFC3339]: http://tools.ietf.org/html/rfc3339 + +### `date_tz`, `modified_tz` +The `date_tz` and `modified_tz` fields are strings with the timezone applying to +the `date` and `modified` fields respectively. The timezone is a [Olsen zoneinfo +database][] identifier. While the `date` field includes timezone offset +information, the `date_tz` field allows proper data operations across Daylight +Savings Time boundaries. + +This field should be treated the same as the `date_tz` property on a +Post entity. + + +Media +----- +The Media entity is a JSON object based on the Post entity. It contains all +properties of the Post entity, with the following additional properties defined: + +### `source` +The `source` field is a string with the URL of the entity's original file. For +image media, this is the source file that intermediate representations are +generated from. For non-image media, this is the attached media file itself. + +### `is_image` +The `is_image` field is a boolean which indicates whether the entity's +associated file should be handled as an image. + +### `attachment_meta` +The `attachment_meta` field is a Media Meta entity. If the file is not an image +(as indicated by the `is_image` field), this is an empty JSON object. + + +Media Meta +---------- +The Media Meta entity is a JSON object with properties relating to the +associated Media entity. The following properties are defined for the entity: + +### `width` +The `width` field is an integer with the original file's width in pixels. + +### `height` +The `height` field is an integer with the original file's height in pixels. + +### `file` +The `file` field is a string with the path to the original file, relative to the +site's upload directory. + +### `sizes` +The `sizes` field is a JSON object mapping intermediate image sizes to image +data objects. The key of each item is the size of the intermediate image as an +internal string representation. The value of each item has the following +properties defined. + +* `file`: The filename of the intermediate file, relative to the directory of + the original file. +* `width`: The width of the intermediate file in pixels. +* `height`: The height of the intermediate file in pixels. +* `mime-type`: The MIME type of the intermediate file. +* `url`: The full URL to the intermediate file. + +### `image_meta` +The `image_meta` field is a JSON object mapping image meta properties to their +values. This data is taken from the EXIF data on the original image. The +following properties are defined. + +* `aperture`: The aperture used to create the original image as a decimal number + (with two decimal places). +* `credit`: Credit for the original image. +* `camera`: The camera used to create the original image. +* `created_timestamp`: When the file was created, as a Unix timestamp. +* `copyright`: Copyright for the original image. +* `focal_length`: The focal length used to create the original image as a + decimal string. +* `iso`: The ISO used to create the original image. +* `shutter_speed`: The shutter speed used to create the original image, as a + decimal string. +* `title`: The original title of the image. + + +Documents +========= + +Index +----- +The Index document is the root endpoint for the API server and describes the +contents and abilities of the API server. + +### Body +The body of an Index document is an Index entity. + +### Example + + { + "name":"My WordPress Site", + "description":"Just another WordPress site", + "URL":"http:\/\/example.com", + "routes": { + "\/": { + "supports": [ "HEAD", "GET" ] + }, + "\/posts": { + "supports": [ "HEAD", "GET", "POST" ], + "accepts_json": true + }, + "\/posts\/": { + "supports": [ "HEAD", "GET", "POST", "PUT", "PATCH", "DELETE" ] + }, + "\/posts\/\/revisions": { + "supports": [ "HEAD", "GET" ] + }, + "\/posts\/\/comments": { + "supports": [ "HEAD", "GET", "POST" ], + "accepts_json":true + } + }, + "meta": { + "links": { + "help":"http:\/\/codex.wordpress.org\/JSON_API" + } + } + } + + +Post +---- +A Post document is defined as the representation of a post item, analogous to an +Atom item. + +### Headers +The following headers are sent when a Post is the main entity: + +* `Link`: + * `rel="alternate"; type=text/html`: The permalink for the Post + * `rel="collection"`: The endpoint of the Post Collection the Post is + contained in + * `rel="replies"`: The endpoint of the associated Comment Collection + * `rel="version-history"`: The endpoint of the Post Collection containing + the revisions of the Post + + +### Body +The body of a Post document is a Post entity. + + +### Example + + HTTP/1.1 200 OK + Date: Mon, 07 Jan 2013 03:35:14 GMT + Last-Modified: Mon, 07 Jan 2013 03:35:14 GMT + Link: ; rel="alternate"; type=text/html + Link: ; rel="author" + Link: ; rel="collection" + Link: ; rel="replies" + Link: ; rel="version-history" + Content-Type: application/json; charset=UTF-8 + + { + "ID":158, + "title":"This is a test!", + "status":"publish", + "type":"post", + "author":{ + "ID":1, + "name":"admin", + "slug":"admin", + "URL":"", + "avatar":"http:\/\/0.gravatar.com\/avatar\/c57c8945079831fa3c19caef02e44614&d=404&r=G", + "meta":{ + "links":{ + "self":"http:\/\/localhost\/wptrunk\/wp-json\/users\/1", + "archives":"http:\/\/localhost\/wptrunk\/wp-json\/users\/1\/posts" + } + } + }, + "content":"Hello.\r\n\r\nHah.", + "parent":0, + "link":"http:\/\/localhost\/wptrunk\/158\/this-is-a-test\/", + "date":"2013-01-07T13:35:14+10:00", + "modified":"2013-01-07T13:49:40+10:00", + "format":"standard", + "slug":"this-is-a-test", + "guid":"http:\/\/localhost\/wptrunk\/?p=158", + "excerpt":"", + "menu_order":0, + "comment_status":"open", + "ping_status":"open", + "sticky":false, + "date_tz":"Australia\/Brisbane", + "date_gmt":"2013-01-07T03:35:14+00:00", + "modified_tz":"Australia\/Brisbane", + "modified_gmt":"2013-01-07T03:49:40+00:00", + "post_thumbnail":[], + "terms":{ + "category":{ + "ID":1, + "name":"Uncategorized", + "slug":"uncategorized", + "group":0, + "parent":0, + "count":4, + "meta":{ + "links":{ + "collection":"http:\/\/localhost\/wptrunk\/wp-json\/taxonomy\/category", + "self":"http:\/\/localhost\/wptrunk\/wp-json\/taxonomy\/category\/terms\/1" + } + } + } + }, + "post_meta":[], + "meta":{ + "links":{ + "self":"http:\/\/localhost\/wptrunk\/wp-json\/posts\/158", + "author":"http:\/\/localhost\/wptrunk\/wp-json\/users\/1", + "collection":"http:\/\/localhost\/wptrunk\/wp-json\/posts", + "replies":"http:\/\/localhost\/wptrunk\/wp-json\/posts\/158\/comments", + "version-history":"http:\/\/localhost\/wptrunk\/wp-json\/posts\/158\/revisions" + } + } + } + + +Post Collection +--------------- +A Post Collection document is defined as a collection of Post entities. + +### Headers +The following headers are sent when a Post Collection is the main entity: + +* `Link`: + * `rel="item"` - Each item in the collection has a corresponding Link header + containing the location of the endpoint for that resource. + + +### Body +The Post Collection document is a JSON array of Post entities. + + +User +---- +The User document describes a member of the site. + +### Body +The body of a User document is a User entity. + + +Appendix A: JSON Schema +======================= +The JSON Schema describing the entities in this document is available in +schema.json. diff --git a/docs/schema.json b/docs/schema.json deleted file mode 100644 index ea09364367..0000000000 --- a/docs/schema.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema#", - "id": "http://wp-api.ryanmccue.info/schema#", - - "definitions": { - "index": { - "title": "Index", - - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The site's name" - }, - "description": { - "type": "string", - "description": "The site's description" - }, - "URL": { - "type": "string", - "description": "The location of the site" - }, - "routes": { - "type": "object", - "description": "The URL patterns that are handled by the API", - "patternProperties": { - ".+": { - "type": "object", - "description": "A single route description", - "properties": { - "supports": { - "type": "array", - "description": "The HTTP methods supported by the endpoint", - "items": { - "enum": [ - "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ] - } - }, - "accepts_json": { - "type": "boolean", - "description": "Whether the endpoint accepts a raw JSON POST body for data", - "default": false - } - }, - "required": ["supports"] - } - } - }, - "meta": { - "type": "object", - "description": "Metadata for the Index entity" - } - }, - - "required": ["name", "description", "URL", "routes", "meta"] - }, - "post": { - "title": "Post", - - "type": "object", - "properties": { - "ID": { - "type": "integer", - "description": "Entity ID" - }, - "title": { - "type": "string", - "description": "Post title" - }, - "date": { - "type": "string", - "description": "Post creation datetime in the local timezone" - }, - "date_tz": { - "type": "string", - "description": "Olsen timezone identifier for the date field" - }, - "date_gmt": { - "type": "string", - "description": "Post creation datetime in UTC" - }, - "modified": { - "type": "string", - "description": "Post last modification datetime in the local timezone" - }, - "modified_tz": { - "type": "string", - "description": "Olsen timezone identifier for the modified field" - }, - "modified_gmt": { - "type": "string", - "description":"Post last modification datetime in UTC" - }, - "status": { - "type": "string", - "description": "Post published status", - "enum": [ - "draft", - "pending", - "private", - "publish", - "trash" - ] - }, - "type": { - "type": "string", - "description": "Post type" - }, - "name": { - "type": "string", - "description": "Post slug (URL identifier)" - }, - "author": { - "type": "object", - "description": "Post author details", - "$ref": "#/definitions/user" - }, - "password": { - "type": "string", - "description": "Post password" - }, - "excerpt": { - "type": "string", - "description": "Short version of the post content for display" - }, - "content": { - "type": "string", - "description": "Post content" - }, - "parent": { - "type": "integer", - "description": "Parent post's ID, 0 for no parent" - }, - "link": { - "type": "string", - "format": "uri", - "description": "Full URL to the post" - }, - "guid": { - "type": "string", - "description": "Globally unique identifier for the post" - }, - "menu_order": { - "type": "integer", - "description": "The post's position in menus", - "default": 0 - }, - "comment_status": { - "type": "string", - "description": "Whether the post is open for commenting", - "enum": [ - "open", - "closed" - ] - }, - "ping_status": { - "type": "string", - "description": "Whether the post is open for pingbacks/trackbacks", - "enum": [ - "open", - "closed" - ] - }, - "sticky": { - "type": "boolean", - "description": "Whether the post is stickied (shown at the top of archives)" - }, - "post_thumbnail": { - "type": [ "object", "array" ], - "description": "Thumbnail image representing the post" - }, - "post_format": { - "type": "string", - "description": "Standardized post format", - "enum": [ - "standard", - "aside", - "gallery", - "image", - "link", - "status" - ] - }, - "terms": { - "type": "object", - "description": "Taxonomic terms" - }, - "post_meta": { - "type": "array", - "description": "Post meta fields" - } - }, - - "required": ["ID", "link"] - }, - "user": { - "title": "User", - - "type": "object", - "properties": { - "ID": { - "type": "integer", - "description": "User ID" - }, - "name": { - "type": "string", - "description": "User's display name" - }, - "slug": { - "type": "string", - "description": "Author slug (URL identifier)" - }, - "URL": { - "type": "string", - "description": "URL to the author's site", - "format": "uri" - }, - "avatar": { - "type": "string", - "description": "URL to the author's avatar image", - "format": "uri" - }, - "meta": { - "type": "object", - "description": "Metadata for the Index entity" - } - }, - - "required": ["ID", "slug", "meta"] - } - } -} \ No newline at end of file diff --git a/docs/schema.md b/docs/schema.md deleted file mode 100644 index 154aea7830..0000000000 --- a/docs/schema.md +++ /dev/null @@ -1,825 +0,0 @@ -Introduction -============ -The API is designed around two types of responses: entities, and collections. -Entities are JSON objects representing internal objects, both abstract and -WordPress objects. Collections are JSON arrays of Entities. - -This document is for clients and providers wanting to ensure full compliance -with the specification. - - -Definitions -========== -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", -"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be -interpreted as described in [RFC2119][]. - -* Provider: A site making the API available for use -* Consumer: An application accessing and interacting with the API -* slug: A URL-friendly human-readable identifier, usually derived from the title - of the entity. - -[RFC2119]: http://tools.ietf.org/html/rfc2119 - - -### ABNF -Augmented Backus-Naur Form (ABNF) is to be interpreted as described in -[RFC5234][]. In addition, the following basic rules are used to describe basic -parsing constructs above the standard JSON parsing rules. - - token = 1* ; DQUOTE must be escaped with "\" - -Note that as per ABNF, literal strings are case insensitive. That is: - - example-field = "id" - example-field = "ID" - -Providers SHOULD use the capitalisation as per this specification to ensure -maximum compatibility with consumers. Consumers SHOULD ignore the case of -literal strings when parsing data. - -[RFC5234]: http://tools.ietf.org/html/rfc5234 - - -Entities -======== - -Index ------ -The Index entity is a JSON object with site properties. The following properties -are defined for the Index entity object. - -### `name` -The `name` field is a string with the site's name. - -### `description` -The `description` field is a string with the site's description. - -### `URL` -The `URL` field is a string with the URL to the site itself. - -### `routes` -The `routes` field is an object with keys as a route and the values as a route -descriptor. - -The route is a string giving the URL template for the route, relative to the API -root. The template contains URL parts separated by forward slashes, with each -URL part either a static string, or a route variable encased in angle brackets. - - route = ( "/" - / *( "/" ( token / route-variable ) ) ) - route-variable = "<" token ">" - -These routes can be converted into URLs by replacing all route variables with -their relevant values, then concatenating the relative URL to the API base. - -The route descriptor is an object with the following defined properties. - -* `supports`: A JSON array of supported HTTP methods (verbs). Possible values - are "HEAD", "GET", "POST", "PUT", "PATCH", "DELETE" -* `accepts_json`: A boolean indicating whether data can be passed directly via a - POST request body. Default for missing properties is false. -* `meta`: An Entity Meta entity. Typical `links` values consist of a `self` link - pointing to the route's full URL. - -### `meta` -The `meta` field is a Entity Meta entity with metadata relating to the entity -representation. - -Typical `links` values for the meta object consist of a `help` key with the -value indicating a human-readable documentation page about the API. - -### Example - - { - "name": "My WordPress Site", - "description": "Just another WordPress site", - "URL": "http:\/\/example.com", - "routes": { - "\/": { - "supports": [ - "HEAD", - "GET" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/" - } - }, - "\/posts": { - "supports": [ - "HEAD", - "GET", - "POST" - ], - "meta": { - "self": "http:\/\/example.com\/wp-json\/posts" - }, - "accepts_json": true - }, - "\/posts\/": { - "supports": [ - "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "accepts_json": true - }, - "\/posts\/\/revisions": { - "supports": [ - "HEAD", - "GET" - ] - }, - "\/posts\/\/comments": { - "supports": [ - "HEAD", - "GET", - "POST" - ], - "accepts_json": true - }, - "\/posts\/\/comments\/": { - "supports": [ - "HEAD", - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "accepts_json": true - }, - }, - "meta": { - "links": { - "help": "https:\/\/github.com\/WP-API\/WP-API", - "profile": "https:\/\/raw.github.com\/WP-API\/WP-API\/master\/docs\/schema.json" - } - } - } - -Post ----- -The Post entity is a JSON object of post properties. Unless otherwise defined, -properties are available in all contexts. The following properties are defined -for the Post entity object: - -### `title` -The `title` field is a string with the post's title. - -### `date`, `date_gmt` -The `date` and `date_gmt` fields are strings with the post's creation date and -time in the local time and UTC respectively. These fields follow the [RFC3339][] -Section 5.6 datetime representation. - - date = date-time - date_gmt = date-time - -[RFC3339]: http://tools.ietf.org/html/rfc3339 - -### `modified`, `modified_gmt` -The `modified` and `modified_gmt` fields are strings with the post's last -modification date and time in the local time and UTC respectively. These fields -follow the [RFC3339][] Section 5.6 datetime representation. - - modified = date-time - modified_gmt = date-time - -### `date_tz`, `modified_tz` -The `date_tz` and `modified_tz` fields are strings with the timezone applying to -the `date` and `modified` fields respectively. The timezone is a [Olsen zoneinfo -database][] identifier. While the `date` and `modified` fields include timezone -offset information, the `date_tz` and `modified_tz` fields allow proper data -operations across Daylight Savings Time boundaries. - -Note that in addition to the normal Olsen timezones, manual offsets may be -given. These manual offsets use the deprecated `Etc/GMT+...` zones and specify -an integer offset in hours from UTC. - - timezone = Olsen-timezone / manual-offset - manual-offset = "Etc/GMT" ("-" / "+") 1*2( DIGIT ) - -Consumers SHOULD use the fields if they perform mathematical operations on the -`date` and `modified` fields (such as adding an hour to the last modification -date) rather than relying on the `time-offset` in the `date` or -`modified` fields. - -[Olsen zoneinfo database]: https://en.wikipedia.org/wiki/Tz_database - -### `status` -The `status` field is a string with the post's status. This status relates to -where the post is in the editorial process. These are usually set values, but -some providers may have extra post statuses. - - post-status = "draft" / "pending" / "private" / "publish" / "trash" / token - -Consumers who encounter an unknown or missing post status SHOULD treat it the -same as a "draft" status. - -### `type` -The `type` field is a string with the post's type. This field is specific to -providers, with the most basic representation being "post". The type of the -post usually relates to the fields in the Post entity, with other types having -additional fields specific to the type. - - post-type = "post" / token - -Consumers who encounter an unknown or missing post type SHOULD treat it the same -as a "post" type. - -### `name` -The `name` field is a string with the post's slug. - -### `author` -The `author` field is a User entity with the user who created the post. - -### `password` -The `password` field is a string with the post's password. A zero-length -password indicates that the post does not have a password. - -Consumers who encounter a missing password MUST treat it the same as a -zero-length password. - -### `content` -The `content` field is a string with the post's content. - -### `excerpt` -The `excerpt` field is a string with the post's excerpt. This is usually a -shortened version of the post content, suitable for displaying in -collection views. - -Consumers who encounter a missing excerpt MAY present a shortened version of the -`content` field instead. - -### `content_raw`, `excerpt_raw` -The `content_raw` and `excerpt_raw` fields are strings with the post's content -and excerpt respectively. Unlike the `content` and `excerpt` fields, the value -has not been passed through internal filtering, and is suitable for editing. - -(Context Availability: `edit`) - -### `parent` -The `parent` field is an integer or JSON object with the post's parent -post ID. A literal zero indicates that the post does not have a parent -post. - - post-parent = "0" / 1*DIGIT - -Consumers who encounter a missing parent ID MUST treat it the same as a parent -post ID of 0. - -Parent fields will be expanded into a full Post entity in the `view` or `edit` -contexts, but only one level deep. The embedded Post entity will be rendered -using the `parent` context. - -In the `parent` context, the field will contain an integer with the post's -parent post ID as above. - -### `link` -The `link` field is a string with the full URL to the post's canonical view. -This is typically the human-readable location of the entity. - -### `guid` -The `guid` field is a string with the post's globally unique identifier (GUID). - -The GUID is typically in URL form, as this is a relatively easy way of ensuring -that the GUID is globally unique. However, consumers MUST NOT treat the GUID as -a URL, and MUST treat the GUID as a string of arbitrary characters. - -### `menu_order` -The `menu_order` field is an integer with the post's sorting position. This is -typically used to affect sorting when displaying the post in menus or lists. -Larger integers should be treated as sorting before smaller integers. - - menu-order = 1*DIGIT / "-" 1*DIGIT - -Consumers who encounter a missing sorting position MUST treat it the same as a -sorting position of 0. - -### `comment_status` -The `comment_status` field is a string with the post's current commenting -status. This field indicates whether users can submit comments to the post. - - post-comment-status = "open" / "closed" / token - -Providers MAY use statuses other than "open" or "closed" to indicate other -statuses. Consumers who encounter an unknown or missing comment status SHOULD -treat it as "closed". - -### `ping_status` -The `ping_status` field is a string with the post's current pingback/trackback -status. This field indicates whether users can submit pingbacks or trackbacks -to the post. - - ping-status = "open" / "closed" / token - -Providers MAY use statuses other than "open" or "closed" to indicate other -statuses. Consumers who encounter an unknown or missing ping status SHOULD treat -it as "closed". - -### `sticky` -The `sticky` field is a boolean indicating whether the post is marked as a -sticky post. Consumers typically display sticky posts before other posts in -collection views. - -### `post_thumbnail` -The `post_thumbnail` field is a Media entity. - -### `post_format` -The `post_format` field is a string with the post format. The post format -indicates how some meta fields should be displayed. For example, posts with the -"link" format may wish to display an extra link to a URL specified in a meta -field or emphasise a link in the post content. - - post-format = "standard" / "aside" / "gallery" / "image" / "link" / "status" / "quote" / "video" / "audio" / "chat" - -Providers MUST NOT use post formats not specified by this specification, unless -specified in a subsequent version of the specification. Consumers MUST treat -unknown post formats as "standard". - -### `terms` -The `terms` field is a Term collection. - -### `post_meta` -The `meta` field is a Metadata entity with metadata relating to the post. - -### `meta` -The `meta` field is a Entity Meta entity with metadata relating to the entity -representation. - -### Example - - { - "ID": 1, - "title": "Hello world!q", - "status": "publish", - "type": "post", - "author": { - "ID": 1, - "name": "admin", - "slug": "admin", - "URL": "", - "avatar": "http:\/\/0.gravatar.com\/avatar\/c57c8945079831fa3c19caef02e44614&d=404&r=G", - "meta": { - "links": { - "self": "http:\/\/example.com\/wp-json\/users\/1", - "archives": "http:\/\/example.com\/wp-json\/users\/1\/posts" - } - }, - "first_name": "", - "last_name": "" - }, - "content": "

Welcome to WordPress. This is your first post. Edit or delete it, then start blogging!<\/p>\n", - "parent": 0, - "link": "http:\/\/example.com\/2013\/06\/02\/hello-world\/", - "date": "2013-06-02T05:28:00+10:00", - "modified": "2013-06-30T13:56:57+10:00", - "format": "standard", - "slug": "hello-world", - "guid": "http:\/\/example.com\/?p=1", - "excerpt": "", - "menu_order": 0, - "comment_status": "open", - "ping_status": "open", - "sticky": false, - "date_tz": "Australia\/Brisbane", - "date_gmt": "2013-06-02T05:28:00+00:00", - "modified_tz": "Australia\/Brisbane", - "modified_gmt": "2013-06-30T03:56:57+00:00", - "password": "", - "post_meta": [ - ], - "meta": { - "links": { - "self": "http:\/\/example.com\/wp-json\/posts\/1", - "author": "http:\/\/example.com\/wp-json\/users\/1", - "collection": "http:\/\/example.com\/wp-json\/posts", - "replies": "http:\/\/example.com\/wp-json\/posts\/1\/comments", - "version-history": "http:\/\/example.com\/wp-json\/posts\/1\/revisions" - } - }, - "featured_image": null, - "terms": { - "category": { - "ID": 1, - "name": "Uncategorized", - "slug": "uncategorized", - "parent": null, - "count": 7, - "meta": { - "links": { - "collection": "http:\/\/example.com\/wp-json\/taxonomies\/category\/terms", - "self": "http:\/\/example.com\/wp-json\/taxonomies\/category\/terms\/1" - } - } - } - } - } - - - -Entity Meta ------------ -The Entity Meta entity is a JSON object with custom metadata relating to the -representation of the parent entity. - -The following properties are defined for the Entity Meta entity object: - -### `links` -The `links` field is a JSON object with hyperlinks to related entities. Each -item's key is a link relation as per the [IANA Link Relations registry][] with -the value of the item being the corresponding link URL. - -Typical link relations are: - -* `self`: A URL pointing to the current entity's location. -* `up`: A URL pointing to the parent entity's location. -* `collection`: A URL pointing to a collection that the entity is a member of. - -[IANA Link Relations registry]: http://www.iana.org/assignments/link-relations/link-relations.xml - - -User ----- -The User entity is a JSON object with user properties. The following properties -are defined for the User entity object: - -### `ID` -The `ID` field is an integer with the user's ID. - -### `name` -The `name` field is a string with the user's display name. - -### `slug` -The `slug` field is a string with the user's slug. - -### `URL` -The `URL` field is a string with the URL to the author's site. This is typically -an external link of the author's choice. - -### `avatar` -The `avatar` field is a string with the URL to the author's avatar image. - -Providers SHOULD ensure that for users without an avatar image, this field is -either zero-length or the URL returns a HTTP 404 error code on access. Consumers -MAY display a default avatar instead of a zero-length or URL which returns -a HTTP 404 error code. - -### `meta` -The `meta` field is a Entity Meta entity with metadata relating to the entity -representation. - - -Metadata --------- -The Metadata entity is a JSON array with metadata fields. Each metadata field is -a JSON object with `id`, `key` and `value` fields. - -### `id` -The `id` field of the metadata field is a positive integer with the internal -metadata ID. - -### `key` -The `key` field of the metadata field is a string with the metadata field name. - -### `value` -The `value` field of the metadata field is a string with the metadata -field value. - - -Comment -------- -The Comment entity is a JSON object with comment properties. The following -properties are defined for the Comment entity object: - -### `ID` -The `ID` field is an integer with the comment's ID. - -### `content` -The `content` field is a string with the comment's content. - -### `status` -The `status` field is a string with the comment's status. This field indicates -whether the comment is in the publishing process, or if it has been deleted or -marked as spam. - - comment-status = "hold" / "approved" / "spam" / "trash" / token - -Providers MAY use other values to indicate other statuses. Consumers who -encounter an unknown or missing status SHOULD treat it as "hold". - -### `type` -The `type` field is a string with the comment's type. This is usually one of the -following, but providers may provide additional values. - - comment-type = "comment" / "trackback" / "pingback" / token - -Providers MAY use other values to indicate other types. Consumers who encounter -an unknown or missing status SHOULD treat it as "comment". - -### `post` -The `post` field is an integer with the parent post for the comment, or a Post -entity describing the parent post. A literal zero indicates that the comment -does not have a parent post. - - comment-post-parent = "0" / 1*DIGIT - -Consumers who encounter a missing post ID MUST treat it the same as a parent -post ID of 0. - -### `parent` -The `post` field is an integer with the parent comment, or a Comment entity -describing the parent comment. A literal zero indicates that the comment does -not have a parent comment. - - comment-parent = "0" / 1*DIGIT - -Consumers who encounter a missing parent ID MUST treat it the same as a parent -comment ID of 0. - -### `author` -The `author` field is a User entity with the comment author's data, or a -User-like object for anonymous authors. The User-like object contains the -following properties: - -#### `ID` -The `ID` property on the User-like object is always set to `0` for anonymous -authors. - -#### `name` -The `name` property on the User-like object is a string with the author's name. - -#### `URL` -The `URL` property on the User-like object is a string with the author's URL. - -#### `avatar` -The `avatar` property on the User-like object is a string with the URL to the -author's avatar image. - -This property should be treated the same as the avatar property on the -User entity. - - -### `date`, `date_gmt` -The `date` and `date_gmt` fields are strings with the post's creation date and -time in the local time and UTC respectively. These fields follow the [RFC3339][] -Section 5.6 datetime representation. - - date = date-time - date_gmt = date-time - -This field should be treated the same as the `date` and `date_gmt` properties on -a Post entity. - -[RFC3339]: http://tools.ietf.org/html/rfc3339 - -### `date_tz`, `modified_tz` -The `date_tz` and `modified_tz` fields are strings with the timezone applying to -the `date` and `modified` fields respectively. The timezone is a [Olsen zoneinfo -database][] identifier. While the `date` field includes timezone offset -information, the `date_tz` field allows proper data operations across Daylight -Savings Time boundaries. - -This field should be treated the same as the `date_tz` property on a -Post entity. - - -Media ------ -The Media entity is a JSON object based on the Post entity. It contains all -properties of the Post entity, with the following additional properties defined: - -### `source` -The `source` field is a string with the URL of the entity's original file. For -image media, this is the source file that intermediate representations are -generated from. For non-image media, this is the attached media file itself. - -### `is_image` -The `is_image` field is a boolean which indicates whether the entity's -associated file should be handled as an image. - -### `attachment_meta` -The `attachment_meta` field is a Media Meta entity. If the file is not an image -(as indicated by the `is_image` field), this is an empty JSON object. - - -Media Meta ----------- -The Media Meta entity is a JSON object with properties relating to the -associated Media entity. The following properties are defined for the entity: - -### `width` -The `width` field is an integer with the original file's width in pixels. - -### `height` -The `height` field is an integer with the original file's height in pixels. - -### `file` -The `file` field is a string with the path to the original file, relative to the -site's upload directory. - -### `sizes` -The `sizes` field is a JSON object mapping intermediate image sizes to image -data objects. The key of each item is the size of the intermediate image as an -internal string representation. The value of each item has the following -properties defined. - -* `file`: The filename of the intermediate file, relative to the directory of - the original file. -* `width`: The width of the intermediate file in pixels. -* `height`: The height of the intermediate file in pixels. -* `mime-type`: The MIME type of the intermediate file. -* `url`: The full URL to the intermediate file. - -### `image_meta` -The `image_meta` field is a JSON object mapping image meta properties to their -values. This data is taken from the EXIF data on the original image. The -following properties are defined. - -* `aperture`: The aperture used to create the original image as a decimal number - (with two decimal places). -* `credit`: Credit for the original image. -* `camera`: The camera used to create the original image. -* `created_timestamp`: When the file was created, as a Unix timestamp. -* `copyright`: Copyright for the original image. -* `focal_length`: The focal length used to create the original image as a - decimal string. -* `iso`: The ISO used to create the original image. -* `shutter_speed`: The shutter speed used to create the original image, as a - decimal string. -* `title`: The original title of the image. - - -Documents -========= - -Index ------ -The Index document is the root endpoint for the API server and describes the -contents and abilities of the API server. - -### Body -The body of an Index document is an Index entity. - -### Example - - { - "name":"My WordPress Site", - "description":"Just another WordPress site", - "URL":"http:\/\/example.com", - "routes": { - "\/": { - "supports": [ "HEAD", "GET" ] - }, - "\/posts": { - "supports": [ "HEAD", "GET", "POST" ], - "accepts_json": true - }, - "\/posts\/": { - "supports": [ "HEAD", "GET", "POST", "PUT", "PATCH", "DELETE" ] - }, - "\/posts\/\/revisions": { - "supports": [ "HEAD", "GET" ] - }, - "\/posts\/\/comments": { - "supports": [ "HEAD", "GET", "POST" ], - "accepts_json":true - } - }, - "meta": { - "links": { - "help":"http:\/\/codex.wordpress.org\/JSON_API" - } - } - } - - -Post ----- -A Post document is defined as the representation of a post item, analogous to an -Atom item. - -### Headers -The following headers are sent when a Post is the main entity: - -* `Link`: - * `rel="alternate"; type=text/html`: The permalink for the Post - * `rel="collection"`: The endpoint of the Post Collection the Post is - contained in - * `rel="replies"`: The endpoint of the associated Comment Collection - * `rel="version-history"`: The endpoint of the Post Collection containing - the revisions of the Post - - -### Body -The body of a Post document is a Post entity. - - -### Example - - HTTP/1.1 200 OK - Date: Mon, 07 Jan 2013 03:35:14 GMT - Last-Modified: Mon, 07 Jan 2013 03:35:14 GMT - Link: ; rel="alternate"; type=text/html - Link: ; rel="author" - Link: ; rel="collection" - Link: ; rel="replies" - Link: ; rel="version-history" - Content-Type: application/json; charset=UTF-8 - - { - "ID":158, - "title":"This is a test!", - "status":"publish", - "type":"post", - "author":{ - "ID":1, - "name":"admin", - "slug":"admin", - "URL":"", - "avatar":"http:\/\/0.gravatar.com\/avatar\/c57c8945079831fa3c19caef02e44614&d=404&r=G", - "meta":{ - "links":{ - "self":"http:\/\/localhost\/wptrunk\/wp-json\/users\/1", - "archives":"http:\/\/localhost\/wptrunk\/wp-json\/users\/1\/posts" - } - } - }, - "content":"Hello.\r\n\r\nHah.", - "parent":0, - "link":"http:\/\/localhost\/wptrunk\/158\/this-is-a-test\/", - "date":"2013-01-07T13:35:14+10:00", - "modified":"2013-01-07T13:49:40+10:00", - "format":"standard", - "slug":"this-is-a-test", - "guid":"http:\/\/localhost\/wptrunk\/?p=158", - "excerpt":"", - "menu_order":0, - "comment_status":"open", - "ping_status":"open", - "sticky":false, - "date_tz":"Australia\/Brisbane", - "date_gmt":"2013-01-07T03:35:14+00:00", - "modified_tz":"Australia\/Brisbane", - "modified_gmt":"2013-01-07T03:49:40+00:00", - "post_thumbnail":[], - "terms":{ - "category":{ - "ID":1, - "name":"Uncategorized", - "slug":"uncategorized", - "group":0, - "parent":0, - "count":4, - "meta":{ - "links":{ - "collection":"http:\/\/localhost\/wptrunk\/wp-json\/taxonomy\/category", - "self":"http:\/\/localhost\/wptrunk\/wp-json\/taxonomy\/category\/terms\/1" - } - } - } - }, - "post_meta":[], - "meta":{ - "links":{ - "self":"http:\/\/localhost\/wptrunk\/wp-json\/posts\/158", - "author":"http:\/\/localhost\/wptrunk\/wp-json\/users\/1", - "collection":"http:\/\/localhost\/wptrunk\/wp-json\/posts", - "replies":"http:\/\/localhost\/wptrunk\/wp-json\/posts\/158\/comments", - "version-history":"http:\/\/localhost\/wptrunk\/wp-json\/posts\/158\/revisions" - } - } - } - - -Post Collection ---------------- -A Post Collection document is defined as a collection of Post entities. - -### Headers -The following headers are sent when a Post Collection is the main entity: - -* `Link`: - * `rel="item"` - Each item in the collection has a corresponding Link header - containing the location of the endpoint for that resource. - - -### Body -The Post Collection document is a JSON array of Post entities. - - -User ----- -The User document describes a member of the site. - -### Body -The body of a User document is a User entity. - - -Appendix A: JSON Schema -======================= -The JSON Schema describing the entities in this document is available in -schema.json. From 1934450e52e25da54456bfc215adf4137e6689e0 Mon Sep 17 00:00:00 2001 From: Chris O'Donnell Date: Tue, 26 Aug 2014 12:04:00 -0400 Subject: [PATCH 431/515] allow periods in jsonp callback function name - now it plays nice with angularjs jsonp calls - Reference: https://github.com/WP-API/WP-API/issues/144#issuecomment-46325232 --- lib/class-wp-json-server.php | 4 ++-- tests/test-json-server.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index a4b05b2000..2e78655f21 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -203,7 +203,7 @@ public function serve_request( $path = null ) { } // Check for invalid characters (only alphanumeric allowed) - if ( ! is_string( $_GET['_jsonp'] ) || preg_match( '/\W/', $_GET['_jsonp'] ) ) { + if ( ! is_string( $_GET['_jsonp'] ) || preg_match( '/\W\./', $_GET['_jsonp'] ) ) { echo $this->json_error( 'json_callback_invalid', __( 'The JSONP callback function is invalid.' ), 400 ); return false; } @@ -613,7 +613,7 @@ public function prepare_response( $data ) { */ public function parse_date( $date, $force_utc = false ) { _deprecated_function( __CLASS__ . '::' . __METHOD__, 'WPAPI-1.1', 'json_parse_date' ); - + return json_parse_date( $date, $force_utc ); } diff --git a/tests/test-json-server.php b/tests/test-json-server.php index 6cd3238f92..653fc7a512 100644 --- a/tests/test-json-server.php +++ b/tests/test-json-server.php @@ -37,7 +37,8 @@ function test_error_to_array() { } /** - * Test the format of errors encoded to json. + * Test the format of errors encoded to json. Include + * a test with periods to be sure it's allowed. */ function test_json_error() { $this->markTestIncomplete('Missing test implementation.'); From 73441e04fa370ac9b56ff9f51d89867e95f09da8 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 27 Aug 2014 20:34:33 -0500 Subject: [PATCH 432/515] Remove logic setting $fields[parent] to post parent before it is overridden by the comment parent. --- lib/class-wp-json-posts.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 58cc06955b..ca807a2878 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1558,12 +1558,6 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen $fields['type'] = 'comment'; } - // Post - if ( 'single' === $context ) { - $parent = get_post( $post['post_parent'], ARRAY_A ); - $fields['parent'] = $this->prepare_post( $parent, 'single-parent' ); - } - // Parent if ( ( 'single' === $context || 'single-parent' === $context ) && (int) $comment->comment_parent ) { $parent_fields = array( 'meta' ); From c03324e1c6bec50cbe43e67402563c6091d3d26c Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 27 Aug 2014 20:52:20 -0500 Subject: [PATCH 433/515] Initial pass at basic tests for get_comments and get_comment. --- tests/test-json-post-comments.php | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/test-json-post-comments.php diff --git a/tests/test-json-post-comments.php b/tests/test-json-post-comments.php new file mode 100644 index 0000000000..9b451da641 --- /dev/null +++ b/tests/test-json-post-comments.php @@ -0,0 +1,44 @@ +post_id = $this->factory->post->create( array( + 'post_title' => 'some-post', + 'post_content' => 'fascinating information', + 'post_type' => 'post', + ) ); + $this->fake_server = $this->getMock( 'WP_JSON_Server' ); + $this->endpoint = new WP_JSON_Posts( $this->fake_server ); + } + + public function test_get_comments() { + $comments = $this->factory->comment->create_post_comments( $this->post_id, 10 ); + + $response = $this->endpoint->get_comments( $this->post_id ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertEquals( 10, count( $data ) ); + } + + public function test_get_comment() { + $comment = $this->factory->comment->create_post_comments( $this->post_id, 1 ); + $comment_id = (int) $comment[0]; + + $response = $this->endpoint->get_comment( $comment_id ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + $this->assertEquals( 200, $response->get_status() ); + } + +} From 4ea3247b9f59596dc8db50d061fc33a09d229103 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 29 Aug 2014 14:11:49 +1000 Subject: [PATCH 434/515] Correct error message string formatting --- lib/class-wp-json-server.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 6803c2db50..bc1720110d 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -446,7 +446,7 @@ protected function get_json_last_error( ) { $error_code_none = defined( 'JSON_ERROR_NONE' ) ? JSON_ERROR_NONE : 0; // prefix error messages with this string - $error_message_prefix = 'Encoding error:'; + $error_message_prefix = __( 'Encoding error: %s' ); switch ( true ) { @@ -458,44 +458,44 @@ protected function get_json_last_error( ) { // use json_last_error_msg() if available case function_exists( 'json_last_error_msg' ) : $error_message = json_last_error_msg(); - return __( sprintf( '%s %s', $error_message_prefix, $error_message ) ); + return sprintf( $error_message_prefix, $error_message ); break; // otherwise use built-in constants case defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code : - return __( sprintf( '%s Maximum stack depth exceeded', $error_message_prefix ) ); + return sprintf( $error_message_prefix, 'Maximum stack depth exceeded' ); break; case defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code : - return __( sprintf( '%s Invalid or malformed JSON', $error_message_prefix ) ); + return sprintf( $error_message_prefix, 'Invalid or malformed JSON' ); break; case defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code : - return __( sprintf( '%s Control character error, possibly incorrectly encoded', $error_message_prefix ) ); + return sprintf( $error_message_prefix, 'Control character error, possibly incorrectly encoded' ); break; case defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code : - return __( sprintf( '%s Syntax error', $error_message_prefix ) ); + return sprintf( $error_message_prefix, 'Syntax error' ); break; case defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code : - return __( sprintf( '%s Malformed UTF-8 characters, possibly incorrectly encoded', $error_message_prefix ) ); + return sprintf( $error_message_prefix, 'Malformed UTF-8 characters, possibly incorrectly encoded' ); break; case defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code : - return __( sprintf( '%s One or more recursive references in the value to be encoded', $error_message_prefix ) ); + return sprintf( $error_message_prefix, 'One or more recursive references in the value to be encoded' ); break; case defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code : - return __( sprintf( '%s One or more NAN or INF values in the value to be encoded', $error_message_prefix ) ); + return sprintf( $error_message_prefix, 'One or more NAN or INF values in the value to be encoded' ); break; case defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code : - return __( sprintf( '%s A value of a type that cannot be encoded was given', $error_message_prefix ) ); + return sprintf( $error_message_prefix, 'A value of a type that cannot be encoded was given' ); break; default : - return __( sprintf( '%s An unknown error occurred', $error_message_prefix ) ); + return sprintf( $error_message_prefix, 'An unknown error occurred' ); } } From 99d67c1ad2a185500996407caf7bbb4f12ba6bda Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 29 Aug 2014 14:15:12 +1000 Subject: [PATCH 435/515] Correct switch formatting and indentation --- lib/class-wp-json-server.php | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index bc1720110d..cb0f94c1ee 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -272,7 +272,6 @@ public function serve_request( $path = null ) { $result = json_encode( $this->prepare_response( $result ) ); - // test for json_encode() error $json_error_message = $this->get_json_last_error(); if ( $json_error_message ) { $json_error_obj = new WP_Error( 'json_encode_error', $json_error_message, array( 'status' => 500 ) ); @@ -444,55 +443,45 @@ protected function get_json_last_error( ) { // just in case JSON_ERROR_NONE is not defined $error_code_none = defined( 'JSON_ERROR_NONE' ) ? JSON_ERROR_NONE : 0; - + // prefix error messages with this string $error_message_prefix = __( 'Encoding error: %s' ); - + switch ( true ) { // return false if no error occurred case $last_error_code === $error_code_none : return false; - break; // use json_last_error_msg() if available case function_exists( 'json_last_error_msg' ) : $error_message = json_last_error_msg(); return sprintf( $error_message_prefix, $error_message ); - break; // otherwise use built-in constants case defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code : return sprintf( $error_message_prefix, 'Maximum stack depth exceeded' ); - break; case defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code : return sprintf( $error_message_prefix, 'Invalid or malformed JSON' ); - break; case defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code : return sprintf( $error_message_prefix, 'Control character error, possibly incorrectly encoded' ); - break; case defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code : return sprintf( $error_message_prefix, 'Syntax error' ); - break; case defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code : return sprintf( $error_message_prefix, 'Malformed UTF-8 characters, possibly incorrectly encoded' ); - break; case defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code : return sprintf( $error_message_prefix, 'One or more recursive references in the value to be encoded' ); - break; case defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code : return sprintf( $error_message_prefix, 'One or more NAN or INF values in the value to be encoded' ); - break; case defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code : return sprintf( $error_message_prefix, 'A value of a type that cannot be encoded was given' ); - break; default : return sprintf( $error_message_prefix, 'An unknown error occurred' ); From 0723cc08f3d1b45140d247a1daf4801d3eac566d Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 29 Aug 2014 14:19:35 +1000 Subject: [PATCH 436/515] Don't prefix error messages Totally changed my mind on this, since they're included in the code. --- lib/class-wp-json-server.php | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index cb0f94c1ee..d755328b48 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -444,9 +444,6 @@ protected function get_json_last_error( ) { // just in case JSON_ERROR_NONE is not defined $error_code_none = defined( 'JSON_ERROR_NONE' ) ? JSON_ERROR_NONE : 0; - // prefix error messages with this string - $error_message_prefix = __( 'Encoding error: %s' ); - switch ( true ) { // return false if no error occurred @@ -455,36 +452,35 @@ protected function get_json_last_error( ) { // use json_last_error_msg() if available case function_exists( 'json_last_error_msg' ) : - $error_message = json_last_error_msg(); - return sprintf( $error_message_prefix, $error_message ); + return json_last_error_msg(); // otherwise use built-in constants case defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code : - return sprintf( $error_message_prefix, 'Maximum stack depth exceeded' ); + return 'Maximum stack depth exceeded'; case defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code : - return sprintf( $error_message_prefix, 'Invalid or malformed JSON' ); + return 'Invalid or malformed JSON'; case defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code : - return sprintf( $error_message_prefix, 'Control character error, possibly incorrectly encoded' ); + return 'Control character error, possibly incorrectly encoded'; case defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code : - return sprintf( $error_message_prefix, 'Syntax error' ); + return 'Syntax error'; case defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code : - return sprintf( $error_message_prefix, 'Malformed UTF-8 characters, possibly incorrectly encoded' ); + return 'Malformed UTF-8 characters, possibly incorrectly encoded'; case defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code : - return sprintf( $error_message_prefix, 'One or more recursive references in the value to be encoded' ); + return 'One or more recursive references in the value to be encoded'; case defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code : - return sprintf( $error_message_prefix, 'One or more NAN or INF values in the value to be encoded' ); + return 'One or more NAN or INF values in the value to be encoded'; case defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code : - return sprintf( $error_message_prefix, 'A value of a type that cannot be encoded was given' ); + return 'A value of a type that cannot be encoded was given'; default : - return sprintf( $error_message_prefix, 'An unknown error occurred' ); + return 'An unknown error occurred'; } } From 48f2da21949969afa13640a33bcae9b055328c39 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 29 Aug 2014 14:33:01 +1000 Subject: [PATCH 437/515] Split error handling into two functions Splits the actual error message generator into json_last_error_msg(), which has full compatibility with PHP 5.5's function. Note: error messages updated to match PHP source; see https://github.com/php/php-src/blob/b2d2b965b2ddb2e013a15a8858420f18614562a9/ext/json/json.c#L840-L872 --- lib/class-wp-json-server.php | 45 +++--------------------------- plugin.php | 53 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index d755328b48..f8f2823c86 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -440,48 +440,11 @@ protected function get_json_last_error( ) { } $last_error_code = json_last_error(); - - // just in case JSON_ERROR_NONE is not defined - $error_code_none = defined( 'JSON_ERROR_NONE' ) ? JSON_ERROR_NONE : 0; - - switch ( true ) { - - // return false if no error occurred - case $last_error_code === $error_code_none : - return false; - - // use json_last_error_msg() if available - case function_exists( 'json_last_error_msg' ) : - return json_last_error_msg(); - - // otherwise use built-in constants - case defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code : - return 'Maximum stack depth exceeded'; - - case defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code : - return 'Invalid or malformed JSON'; - - case defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code : - return 'Control character error, possibly incorrectly encoded'; - - case defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code : - return 'Syntax error'; - - case defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code : - return 'Malformed UTF-8 characters, possibly incorrectly encoded'; - - case defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code : - return 'One or more recursive references in the value to be encoded'; - - case defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code : - return 'One or more NAN or INF values in the value to be encoded'; - - case defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code : - return 'A value of a type that cannot be encoded was given'; - - default : - return 'An unknown error occurred'; + if ( ( defined( 'JSON_ERROR_NONE' ) && $last_error_code === JSON_ERROR_NONE ) || empty( $last_error_code ) ) { + return false; } + + return json_last_error_msg(); } /** diff --git a/plugin.php b/plugin.php index 04c0be125e..a32962e156 100644 --- a/plugin.php +++ b/plugin.php @@ -621,3 +621,56 @@ function json_handle_deprecated_argument( $function, $message, $version ) { header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) ); } + +if ( ! function_exists( 'json_last_error_msg' ) ): +/** + * Returns the error string of the last json_encode() or json_decode() call + * + * @internal This is a compatibility function for PHP <5.5 + * + * @return boolean|string Returns the error message on success, "No Error" if no error has occurred, or FALSE on failure. + */ +function json_last_error_msg() { + // see https://core.trac.wordpress.org/ticket/27799 + if ( ! function_exists( 'json_last_error' ) ) { + return false; + } + + $last_error_code = json_last_error(); + + // just in case JSON_ERROR_NONE is not defined + $error_code_none = defined( 'JSON_ERROR_NONE' ) ? JSON_ERROR_NONE : 0; + + switch ( true ) { + case $last_error_code === $error_code_none : + return 'No error'; + + case defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code : + return 'Maximum stack depth exceeded'; + + case defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code : + return 'State mismatch (invalid or malformed JSON)'; + + case defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code : + return 'Control character error, possibly incorrectly encoded'; + + case defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code : + return 'Syntax error'; + + case defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code : + return 'Malformed UTF-8 characters, possibly incorrectly encoded'; + + case defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code : + return 'Recursion detected'; + + case defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code : + return 'Inf and NaN cannot be JSON encoded'; + + case defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code : + return 'Type is not supported'; + + default : + return 'An unknown error occurred'; + } +} +endif; From 5fbcc3562a1870ebc9ca7c62e0a88308273f1135 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 29 Aug 2014 15:14:16 +1000 Subject: [PATCH 438/515] Tweak switch for coding standards --- plugin.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/plugin.php b/plugin.php index a32962e156..640844b6f9 100644 --- a/plugin.php +++ b/plugin.php @@ -642,34 +642,34 @@ function json_last_error_msg() { $error_code_none = defined( 'JSON_ERROR_NONE' ) ? JSON_ERROR_NONE : 0; switch ( true ) { - case $last_error_code === $error_code_none : + case $last_error_code === $error_code_none: return 'No error'; - case defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code : + case defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code: return 'Maximum stack depth exceeded'; - case defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code : + case defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code: return 'State mismatch (invalid or malformed JSON)'; - case defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code : + case defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code: return 'Control character error, possibly incorrectly encoded'; - case defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code : + case defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code: return 'Syntax error'; - case defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code : + case defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code: return 'Malformed UTF-8 characters, possibly incorrectly encoded'; - case defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code : + case defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code: return 'Recursion detected'; - case defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code : + case defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code: return 'Inf and NaN cannot be JSON encoded'; - case defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code : + case defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code: return 'Type is not supported'; - default : + default: return 'An unknown error occurred'; } } From 7e5392bbab19b9f3225371caa0a9560a16627e76 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 29 Aug 2014 15:34:19 +1000 Subject: [PATCH 439/515] Switch to SVG for build status image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e0f772453..fa7425928c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Access your WordPress site's data through an easy-to-use HTTP REST API. -[![Build Status](https://travis-ci.org/WP-API/WP-API.png?branch=master)](https://travis-ci.org/WP-API/WP-API) +[![Build Status](https://travis-ci.org/WP-API/WP-API.svg?branch=master)](https://travis-ci.org/WP-API/WP-API) ## About From 04ff29f933093dfdf4614326ac1061275ccd38d6 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 29 Aug 2014 15:52:58 +1000 Subject: [PATCH 440/515] Document which query variables are actually available --- docs/routes/routes.md | 102 +++++++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 15 deletions(-) diff --git a/docs/routes/routes.md b/docs/routes/routes.md index 6f236d7f1d..03581da84b 100644 --- a/docs/routes/routes.md +++ b/docs/routes/routes.md @@ -88,21 +88,93 @@ posts. ### Input #### `filter` -The `filter` parameter controls the query parameters. It is essentially a -subset of the parameters available to [`WP_Query`](http://codex.wordpress.org/Class_Reference/WP_Query). - -The parameter should be an array of the following key/value pairs: - -* `post_status` - Comma-separated list of [status - values](http://codex.wordpress.org/Class_Reference/WP_Query#Status_Parameters). - Default is "publish". (string) -* `posts_per_page` - Number of posts to retrieve, use `-1` for all posts. Default - is set by the site. (integer) -* `offset` - Number of posts to skip. Default is 0. (integer) -* `orderby` - Parameter to search by, as per [WP Query](http://codex.wordpress.org/Class_Reference/WP_Query#Order_.26_Orderby_Parameters). Default is - "date". (string) -* `order` - Order to sort by. Default is "DESC". (string, "ASC" or "DESC") -* `s` - Keyword to search for. (string) +The `filter` parameter controls the parameters used to query for posts. + +**Note:** Only "public" query variables are available via the API, as not all +query variables are safe to expose. "Private" query variables are also available +when authenticated as a user with `edit_posts`. Other query variables can be +registered via the `query_vars` filter, or `json_query_vars` for API-specific +query variables. + +Extended documentation on the query variables is available from +[the codex](http://codex.wordpress.org/Class_Reference/WP_Query). + +The following query variables are available to the API: + +* `m` +* `p` +* `posts` +* `w` +* `cat` +* `withcomments` +* `withoutcomments` +* `s` +* `search` +* `exact` +* `sentence` +* `calendar` +* `page` +* `paged` +* `more` +* `tb` +* `pb` +* `author` +* `order` +* `orderby` +* `year` +* `monthnum` +* `day` +* `hour` +* `minute` +* `second` +* `name` +* `category_name` +* `tag` +* `feed` +* `author_name` +* `static` +* `pagename` +* `page_id` +* `error` +* `comments_popup` +* `attachment` +* `attachment_id` +* `subpost` +* `subpost_id` +* `preview` +* `robots` +* `taxonomy` +* `term` +* `cpage` +* `post_type` + +In addition, the following are available when authenticated as a user with +`edit_posts`: + +* `offset` +* `posts_per_page` +* `posts_per_archive_page` +* `showposts` +* `nopaging` +* `post_type` +* `post_status` +* `category__in` +* `category__not_in` +* `category__and` +* `tag__in` +* `tag__not_in` +* `tag__and` +* `tag_slug__in` +* `tag_slug__and` +* `tag_id` +* `post_mime_type` +* `perm` +* `comments_per_page` +* `post__in` +* `post__not_in` +* `post_parent` +* `post_parent__in` +* `post_parent__not_in` ``` GET /posts?filter[posts_per_page]=8&filter[order]=ASC From 433567a9d79afe12822b67bd2187505552712132 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 31 Aug 2014 10:18:18 -0500 Subject: [PATCH 441/515] Add test for get request of comment with parent. --- tests/test-json-post-comments.php | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/test-json-post-comments.php b/tests/test-json-post-comments.php index 9b451da641..b01f32e27d 100644 --- a/tests/test-json-post-comments.php +++ b/tests/test-json-post-comments.php @@ -31,9 +31,27 @@ public function test_get_comments() { $this->assertEquals( 10, count( $data ) ); } + private function _create_comment( $args = array() ) { + $default_args = array( + 'comment_post_ID' => $this->post_id, + ); + $comment_args = wp_parse_args( $args, $default_args ); + + return $this->factory->comment->create_object( $comment_args ); + } + public function test_get_comment() { - $comment = $this->factory->comment->create_post_comments( $this->post_id, 1 ); - $comment_id = (int) $comment[0]; + $comment_id = $this->_create_comment(); + + $response = $this->endpoint->get_comment( (int) $comment_id ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + $this->assertEquals( 200, $response->get_status() ); + } + + public function test_get_comment_with_parent() { + $comment_parent_id = $this->_create_comment(); + $comment_id = $this->_create_comment( array( 'comment_parent' => (int) $comment_parent_id ) ); $response = $this->endpoint->get_comment( $comment_id ); $this->assertNotInstanceOf( 'WP_Error', $response ); From 4c12d269daab8326cd67e22aa214e38f2bc45799 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 31 Aug 2014 10:40:03 -0500 Subject: [PATCH 442/515] Minor sprintf() spacing fixes for WP_JSON_Meta class. --- lib/class-wp-json-meta.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/class-wp-json-meta.php b/lib/class-wp-json-meta.php index 61e1c4cf6c..bb237fa55b 100644 --- a/lib/class-wp-json-meta.php +++ b/lib/class-wp-json-meta.php @@ -156,7 +156,7 @@ protected function prepare_meta( $parent_id, $data, $is_raw = false ) { // Don't expose protected fields. if ( is_protected_meta( $key ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $key ), array( 'status' => 403 ) ); + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.' ), $key ), array( 'status' => 403 ) ); } // Normalize serialized strings @@ -166,7 +166,7 @@ protected function prepare_meta( $parent_id, $data, $is_raw = false ) { // Don't expose serialized data if ( is_serialized( $value ) || ! is_string( $value ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s contains serialized data.'), $key ), array( 'status' => 403 ) ); + return new WP_Error( 'json_meta_protected', sprintf( __( '%s contains serialized data.' ), $key ), array( 'status' => 403 ) ); } $meta = array( @@ -180,7 +180,7 @@ protected function prepare_meta( $parent_id, $data, $is_raw = false ) { /** * Update/add/delete meta for an object. - * + * * Meta data is expected to be sent in the same format as it's output: * * { @@ -269,11 +269,11 @@ public function update_meta( $id, $mid, $data ) { } if ( is_protected_meta( $current->meta_key ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $current->meta_key ), array( 'status' => 403 ) ); + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.' ), $current->meta_key ), array( 'status' => 403 ) ); } if ( is_protected_meta( $data['key'] ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $data['key'] ), array( 'status' => 403 ) ); + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.' ), $data['key'] ), array( 'status' => 403 ) ); } // update_metadata_by_mid will return false if these are equal, so check @@ -346,7 +346,7 @@ public function add_meta( $id, $data ) { } if ( is_protected_meta( $data['key'] ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $data['key'] ), array( 'status' => 403 ) ); + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.' ), $data['key'] ), array( 'status' => 403 ) ); } $meta_key = wp_slash( $data['key'] ); @@ -399,7 +399,7 @@ public function delete_meta( $id, $mid ) { } if ( is_protected_meta( $current->meta_key ) ) { - return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.'), $current->meta_key ), array( 'status' => 403 ) ); + return new WP_Error( 'json_meta_protected', sprintf( __( '%s is marked as a protected field.' ), $current->meta_key ), array( 'status' => 403 ) ); } if ( ! delete_metadata_by_mid( $this->type, $mid ) ) { From ddf31a05ad7ff04f06db15453f9a1595bce98fbe Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 31 Aug 2014 11:11:32 -0500 Subject: [PATCH 443/515] Correct fatal error in WP_JSON_Meta_Posts->insert_post_meta(). Undefined method WP_JSON_Meta_Posts::handle_post_meta_action(). Renamed to new class method $this->handle_inline_meta(). --- lib/class-wp-json-meta-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-meta-posts.php b/lib/class-wp-json-meta-posts.php index 3e30cb16c3..2e3fb412f7 100644 --- a/lib/class-wp-json-meta-posts.php +++ b/lib/class-wp-json-meta-posts.php @@ -116,7 +116,7 @@ public function add_post_meta_data( $data, $post, $context ) { public function insert_post_meta( $post, $data ) { // Post meta if ( ! empty( $data['post_meta'] ) ) { - $result = $this->handle_post_meta_action( $post['ID'], $data['post_meta'] ); + $result = $this->handle_inline_meta( $post['ID'], $data['post_meta'] ); if ( is_wp_error( $result ) ) { return $result; From 062eb173cbedbab66815041983f88430bc7c62c7 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 31 Aug 2014 12:24:45 -0500 Subject: [PATCH 444/515] Set meta id to an integer when checking against the parent id. Resolves issue where creating meta for a post_id was returning an error. --- lib/class-wp-json-meta.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-meta.php b/lib/class-wp-json-meta.php index bb237fa55b..80bb13ee72 100644 --- a/lib/class-wp-json-meta.php +++ b/lib/class-wp-json-meta.php @@ -134,7 +134,7 @@ public function get_meta( $id, $mid ) { return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); } - if ( absint( $meta->$parent_column ) !== $id ) { + if ( absint( $meta->$parent_column ) !== (int) $id ) { return new WP_Error( 'json_meta_' . $this->type . '_mismatch', __( 'Meta does not belong to this object' ), array( 'status' => 400 ) ); } From 03faed1a168056207e50d348b22f109e433cabb6 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 31 Aug 2014 12:30:36 -0500 Subject: [PATCH 445/515] Create a new Response object in WP_JSON_Meta_Posts->add_meta(). Resolves PHP warning that Headers were already sent from duplicate use of json_ensure_response on the the same response object. --- lib/class-wp-json-meta-posts.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-meta-posts.php b/lib/class-wp-json-meta-posts.php index 2e3fb412f7..a3d98a7545 100644 --- a/lib/class-wp-json-meta-posts.php +++ b/lib/class-wp-json-meta-posts.php @@ -71,10 +71,12 @@ public function add_meta( $id, $data ) { return $response; } - $response = json_ensure_response( $response ); $data = (object) $response->get_data(); + $response = new WP_JSON_Response(); $response->header( 'Location', json_url( '/posts/' . $id . '/meta/' . $data->ID ) ); + $response->set_data( $data ); + $response = json_ensure_response( $response ); return $response; } From badb878626b85542ec45d0bda3bcbc1c53b78cc9 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 31 Aug 2014 12:59:04 -0500 Subject: [PATCH 446/515] Fix test_add_meta() to expect an object response. --- tests/test-json-posts-meta.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test-json-posts-meta.php b/tests/test-json-posts-meta.php index 3b06f9f3d3..2980a5aa3e 100644 --- a/tests/test-json-posts-meta.php +++ b/tests/test-json-posts-meta.php @@ -204,9 +204,9 @@ public function test_add_meta() { $this->assertEquals( 'testvalue', $meta[0] ); $data = $response->get_data(); - $this->assertArrayHasKey( 'ID', $data ); - $this->assertEquals( 'testkey', $data['key'] ); - $this->assertEquals( 'testvalue', $data['value'] ); + $this->assertObjectHasAttribute( 'ID', $data ); + $this->assertEquals( 'testkey', $data->key ); + $this->assertEquals( 'testvalue', $data->value ); } public function test_add_meta_no_post_id() { From 9c5e634aa019f81eaf5bf4bd684c7d6946320711 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 31 Aug 2014 13:00:06 -0500 Subject: [PATCH 447/515] Small spacing fixes for WP_Test_JSON_Posts_Meta class. --- tests/test-json-posts-meta.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test-json-posts-meta.php b/tests/test-json-posts-meta.php index 2980a5aa3e..b23de49180 100644 --- a/tests/test-json-posts-meta.php +++ b/tests/test-json-posts-meta.php @@ -13,9 +13,9 @@ public function setUp() { $this->user = $this->factory->user->create(); wp_set_current_user( $this->user ); $this->user_obj = wp_get_current_user(); - $this->user_obj->add_role('author'); + $this->user_obj->add_role( 'author' ); - $this->fake_server = $this->getMock('WP_JSON_Server'); + $this->fake_server = $this->getMock( 'WP_JSON_Server' ); $this->endpoint = new WP_JSON_Meta_Posts( $this->fake_server ); } @@ -306,10 +306,10 @@ public function test_add_meta_serialized_string() { } public function test_add_meta_failed_get() { - $this->endpoint = $this->getMock('WP_JSON_Meta_Posts', array('get_meta'), array( $this->fake_server ) ); + $this->endpoint = $this->getMock( 'WP_JSON_Meta_Posts', array('get_meta'), array( $this->fake_server ) ); $test_error = new WP_Error( 'json_test_error', 'Test error' ); - $this->endpoint->expects($this->any())->method('get_meta')->will($this->returnValue($test_error)); + $this->endpoint->expects( $this->any() )->method( 'get_meta' )->will( $this->returnValue( $test_error ) ); $post_id = $this->factory->post->create(); $data = array( From 25a1a9370a388da08d12a0ef257fb72fb110bba3 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 1 Sep 2014 22:17:14 -0500 Subject: [PATCH 448/515] Correct endpoints class for base unit tests. --- tests/test-json-post-comments.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-json-post-comments.php b/tests/test-json-post-comments.php index b01f32e27d..a22d530314 100644 --- a/tests/test-json-post-comments.php +++ b/tests/test-json-post-comments.php @@ -17,7 +17,7 @@ public function setUp() { 'post_type' => 'post', ) ); $this->fake_server = $this->getMock( 'WP_JSON_Server' ); - $this->endpoint = new WP_JSON_Posts( $this->fake_server ); + $this->endpoint = new WP_JSON_Comments( $this->fake_server ); } public function test_get_comments() { From 23a29122d92150a3c21c920c88241e48f4d43453 Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Sat, 6 Sep 2014 12:58:10 -0400 Subject: [PATCH 449/515] Enqueue client js in admin --- plugin.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin.php b/plugin.php index 05b977f8d5..2ddcc3f345 100644 --- a/plugin.php +++ b/plugin.php @@ -243,6 +243,7 @@ function json_register_scripts() { wp_localize_script( 'wp-api', 'WP_API_Settings', $settings ); } add_action( 'wp_enqueue_scripts', 'json_register_scripts', -100 ); +add_action( 'admin_enqueue_scripts', 'json_register_scripts', -100 ); /** * Add the API URL to the WP RSD endpoint. From 1e1eb264fb9f26c16c13c6086b08df49c6dc524a Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 7 Sep 2014 19:01:23 -0500 Subject: [PATCH 450/515] Revert deprecated handle_post_meta_action() and create handle_get_post_meta() method. Helper methods for post_meta functionality that still occur within Post routes. First steps to fix functionality break in #358. --- lib/class-wp-json-posts.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 09846e0f82..b13db061b7 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1205,6 +1205,31 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen return apply_filters( 'json_prepare_comment', $data, $comment, $context ); } + /** + * Update/add/delete meta for a post. + * + * @param int $post_id + * @param array $data + * @return bool|WP_Error + */ + protected function handle_post_meta_action( $post_id, $data ) { + $handler = new WP_JSON_Meta_Posts( $this->server ); + + return $handler->handle_inline_meta( $post_id, $data['post_meta'] ); + } + + /** + * Retrieve all meta for a post. + * + * @param int $post_id Post ID + * @return (array[]|WP_Error) List of meta object data on success, WP_Error otherwise + */ + protected function handle_get_post_meta( $post_id ) { + $handler = new WP_JSON_Meta_Posts( $this->server ); + + return $handler->get_all_meta( $post_id ); + } + /** * Retrieve custom fields for object * From ff2dd1288bec69860522936cd72803afef156e2e Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 7 Sep 2014 19:02:25 -0500 Subject: [PATCH 451/515] Delete deprecated handle_post_meta_action method. --- lib/class-wp-json-posts.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index b13db061b7..4725c6876c 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1334,22 +1334,6 @@ protected function prepare_meta( $post, $data, $is_raw = false ) { return $handler->_deprecated_call( 'prepare_meta', array( $post, $data, $is_raw ) ); } - /** - * Update/add/delete meta for an object - * - * @deprecated WPAPI-1.2 - * - * @param array $data - * @param int $parent_id - * @return bool|WP_Error - */ - protected function handle_post_meta_action( $post_id, $data ) { - _deprecated_function( 'WP_JSON_Posts::handle_post_meta_action', 'WPAPI-1.2', 'WP_JSON_Meta_Posts::handle_inline_meta' ); - - $handler = new WP_JSON_Meta_Posts( $this->server ); - return $handler->_deprecated_call( 'handle_inline_meta', array( $post, $data, $is_raw ) ); - } - /** * Check if the data provided is valid data * From 3274926019671b581b06c9066351a63626665506 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 7 Sep 2014 19:05:34 -0500 Subject: [PATCH 452/515] Add post_meta handling back into create and edit post actions. --- lib/class-wp-json-posts.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 4725c6876c..b85c639550 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -1085,6 +1085,14 @@ protected function insert_post( $data ) { $post['ID'] = $post_ID; } + // Post meta + if ( ! empty( $data['post_meta'] ) ) { + $result = $this->handle_post_meta_action( $post_ID, $data ); + if ( is_wp_error( $result ) ) { + return $result; + } + } + // Sticky if ( isset( $data['sticky'] ) ) { if ( $data['sticky'] ) { From ffd2fd50664b24061c170d20f00fe08ce6e6e59a Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 7 Sep 2014 19:06:13 -0500 Subject: [PATCH 453/515] Add post_meta fields into posts response. --- lib/class-wp-json-posts.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index b85c639550..092ba48225 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -714,6 +714,7 @@ protected function prepare_post( $post, $context = 'view' ) { 'content_raw' => $post['post_content'], 'excerpt_raw' => $post['post_excerpt'], 'guid_raw' => $post['guid'], + 'post_meta' => $this->handle_get_post_meta( $post['ID'] ), ); // Dates From 8a1ea338767606cde5d84558e07b67f85c000222 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 7 Sep 2014 19:06:38 -0500 Subject: [PATCH 454/515] Unit test to create post with meta value included. --- tests/test-json-posts.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test-json-posts.php b/tests/test-json-posts.php index 5c77719fc4..e552e0e551 100644 --- a/tests/test-json-posts.php +++ b/tests/test-json-posts.php @@ -528,6 +528,23 @@ function test_create_post_sticky_false() { $this->assertFalse( is_sticky( $response_data['ID'] ) ); } + function test_create_post_with_meta() { + $data = $this->set_data( array( + 'post_meta' => array( + array( + 'key' => 'testkey', + 'value' => 'testvalue', + ), + ) ) ); + + $response = $this->endpoint->create_post( $data ); + $response = json_ensure_response( $response ); + + $response_data = $response->get_data(); + $post_meta_value = get_post_meta( $response_data['ID'], 'testkey', true ); + $this->assertEquals( 'testvalue', $post_meta_value ); + } + function test_get_post() { $response = $this->endpoint->get_post( $this->post_id ); From e035a40e3c6e9254a2af8cd43da427cf06dfd3d8 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 7 Sep 2014 22:31:50 -0500 Subject: [PATCH 455/515] Pass edit context to get_post for create and edit post responses. --- lib/class-wp-json-posts.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 092ba48225..ca6d3fdd5a 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -290,7 +290,7 @@ public function create_post( $data ) { return $result; } - $response = json_ensure_response( $this->get_post( $result ) ); + $response = json_ensure_response( $this->get_post( $result, 'edit' ) ); $response->set_status( 201 ); $response->header( 'Location', json_url( '/posts/' . $result ) ); @@ -403,7 +403,7 @@ public function edit_post( $id, $data, $_headers = array() ) { return $retval; } - return $this->get_post( $id ); + return $this->get_post( $id, 'edit' ); } /** From 3c81becc1f1950f0b73c9b0697aeac679b204130 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sun, 7 Sep 2014 22:32:39 -0500 Subject: [PATCH 456/515] Unit test for creating a post with multiple meta fields. --- tests/test-json-posts.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test-json-posts.php b/tests/test-json-posts.php index e552e0e551..606f7b029c 100644 --- a/tests/test-json-posts.php +++ b/tests/test-json-posts.php @@ -539,12 +539,39 @@ function test_create_post_with_meta() { $response = $this->endpoint->create_post( $data ); $response = json_ensure_response( $response ); + $this->check_create_response( $response ); $response_data = $response->get_data(); $post_meta_value = get_post_meta( $response_data['ID'], 'testkey', true ); $this->assertEquals( 'testvalue', $post_meta_value ); } + function test_create_post_with_multiple_meta() { + $data = $this->set_data( array( + 'post_meta' => array( + array( + 'key' => 'some_meta', + 'value' => 'some_value', + ), + array( + 'key' => 'some_other_meta', + 'value' => 'some_other_value', + ), + ) ) ); + + $response = $this->endpoint->create_post( $data ); + $response = json_ensure_response( $response ); + $this->check_create_response( $response ); + + $response_data = $response->get_data(); + $this->assertEquals( 2, count( $response_data['post_meta'] ) ); + + $post_meta_first_value = get_post_meta( $response_data['ID'], 'some_meta', true ); + $post_meta_second_value = get_post_meta( $response_data['ID'], 'some_other_meta', true ); + $this->assertEquals( 'some_value', $post_meta_first_value ); + $this->assertEquals( 'some_other_value', $post_meta_second_value ); + } + function test_get_post() { $response = $this->endpoint->get_post( $this->post_id ); From eed73da3ac0a17015672b44dcb16fd46ce606df9 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Tue, 9 Sep 2014 18:45:24 -0500 Subject: [PATCH 457/515] Unit test to confirm post_meta is included in edit context of get post response. --- tests/test-json-posts.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test-json-posts.php b/tests/test-json-posts.php index 606f7b029c..544bd866b3 100644 --- a/tests/test-json-posts.php +++ b/tests/test-json-posts.php @@ -542,6 +542,8 @@ function test_create_post_with_meta() { $this->check_create_response( $response ); $response_data = $response->get_data(); + $this->assertEquals( 1, count( $response_data['post_meta'] ) ); + $post_meta_value = get_post_meta( $response_data['ID'], 'testkey', true ); $this->assertEquals( 'testvalue', $post_meta_value ); } @@ -586,6 +588,24 @@ function test_get_post() { $this->check_get_post_response( $response, $this->post_obj ); } + function test_get_post_edit_context() { + add_post_meta( $this->post_id, 'some_meta', 'some_value', true ); + add_post_meta( $this->post_id, 'some_other_meta', 'some_other_value', true ); + + $response = $this->endpoint->get_post( $this->post_id, 'edit' ); + + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + $this->check_get_post_response( $response, $this->post_obj ); + + $headers = $response->get_headers(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertArrayHasKey( 'Last-Modified', $headers ); + + $response_data = $response->get_data(); + $this->assertEquals( 2, count( $response_data['post_meta'] ) ); + } + function test_get_revisions() { wp_update_post( array( 'post_content' => 'This content is better.', 'ID' => $this->post_id ) ); wp_update_post( array( 'post_content' => 'This content is marvelous.', 'ID' => $this->post_id ) ); From d3b970a187d25d00f9618bbd88ef0d68dc34e1b2 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 15:27:09 -0500 Subject: [PATCH 458/515] Create global function to check read permissions on objects. --- plugin.php | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/plugin.php b/plugin.php index 2ddcc3f345..cddf0b903e 100644 --- a/plugin.php +++ b/plugin.php @@ -498,6 +498,48 @@ function json_ensure_response( $response ) { return new WP_JSON_Response( $response ); } +/** + * Check if we have permissions to read an object. + * + * @param object $object Post|User|Term object. + * @param string $type Slug of object being passed. + * @return boolean Can we read it? + */ +function json_check_read_permission( $object, $type = 'post' ) { + if ( 'post' == $type ) { + $post_type = get_post_type_object( $object['post_type'] ); + + // Ensure the post type can be read + if ( ! $post_type->show_in_json ) { + return false; + } + + // Can we read the post? + if ( 'publish' === $object['post_status'] || current_user_can( $post_type->cap->read_post, $object['ID'] ) ) { + $permisson = true; + } + + // Can we read the parent if we're inheriting? + if ( 'inherit' === $object['post_status'] && $object['post_parent'] > 0 ) { + $parent = get_post( $object['post_parent'], ARRAY_A ); + + if ( json_check_read_permission( $parent ) ) { + $permisson = true; + } + } + + // If we don't have a parent, but the status is set to inherit, assume + // it's published (as per get_post_status()) + if ( 'inherit' === $object['post_status'] ) { + $permisson = true; + } + + $permisson = false; + } + + return apply_filters( "json_read_{$object}_permission", $permission, $object ); +} + /** * Parse an RFC3339 timestamp into a DateTime. * From d341e366347d603a6e2a46b53a416c00b32683c8 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 15:28:27 -0500 Subject: [PATCH 459/515] Create global function to check edit permissions on objects. --- plugin.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/plugin.php b/plugin.php index cddf0b903e..af05736d95 100644 --- a/plugin.php +++ b/plugin.php @@ -540,6 +540,27 @@ function json_check_read_permission( $object, $type = 'post' ) { return apply_filters( "json_read_{$object}_permission", $permission, $object ); } +/** + * Check if we have permissions to edit an object. + * + * @param object $object Post|User|Term object. + * @param string $type Slug of object being passed. + * @return boolean Can we edit it? + */ +function json_check_edit_permission( $object, $type = 'post' ) { + if ( 'post' == $type ) { + $post_type = get_post_type_object( $object['post_type'] ); + + if ( ! current_user_can( $post_type->cap->edit_post, $object['ID'] ) ) { + $permission = true; + } + + $permission = false; + } + + return apply_filters( "json_edit_{$object}_permission", $permission, $object ); +} + /** * Parse an RFC3339 timestamp into a DateTime. * From 8737046cde57e11655c47cf4535ee01acaa4aa73 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 15:30:43 -0500 Subject: [PATCH 460/515] Deprecate permission check methods in Post controller in favor of global functions. --- lib/class-wp-json-posts.php | 81 +++++++++++++------------------------ 1 file changed, 28 insertions(+), 53 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 09846e0f82..5c33cea986 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -202,59 +202,6 @@ public function get_posts( $filter = array(), $context = 'view', $type = 'post', return $response; } - /** - * Check if we can read a post - * - * Correctly handles posts with the inherit status. - * @param array $post Post data - * @return boolean Can we read it? - */ - protected function check_read_permission( $post ) { - $post_type = get_post_type_object( $post['post_type'] ); - - // Ensure the post type can be read - if ( ! $post_type->show_in_json ) { - return false; - } - - // Can we read the post? - if ( 'publish' === $post['post_status'] || current_user_can( $post_type->cap->read_post, $post['ID'] ) ) { - return true; - } - - // Can we read the parent if we're inheriting? - if ( 'inherit' === $post['post_status'] && $post['post_parent'] > 0 ) { - $parent = get_post( $post['post_parent'], ARRAY_A ); - - if ( $this->check_read_permission( $parent ) ) { - return true; - } - } - - // If we don't have a parent, but the status is set to inherit, assume - // it's published (as per get_post_status()) - if ( 'inherit' === $post['post_status'] ) { - return true; - } - - return false; - } - - /** - * Check if we can edit a post - * @param array $post Post data - * @return boolean Can we edit it? - */ - protected function check_edit_permission( $post ) { - $post_type = get_post_type_object( $post['post_type'] ); - - if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { - return false; - } - - return true; - } - /** * Create a new post for any registered post type. * @@ -1339,4 +1286,32 @@ protected function is_valid_meta_data( $data ) { $handler = new WP_JSON_Meta_Posts( $this->server ); return $handler->_deprecated_call( 'is_valid_meta_data', array( $post, $data, $is_raw ) ); } + + /** + * Check if we can read a post + * + * @deprecated WPAPI-1.2 + * + * @param array $post Post data + * @return boolean Can we read it? + */ + protected function check_read_permission( $post ) { + _deprecated_function( 'WP_JSON_Posts::check_read_permission', 'WPAPI-1.2', 'json_check_read_permission' ); + + return json_check_read_permission( $post ); + } + + /** + * Check if we can edit a post + * + * @deprecated WPAPI-1.2 + * + * @param array $post Post data + * @return boolean Can we edit it? + */ + protected function check_edit_permission( $post ) { + _deprecated_function( 'WP_JSON_Posts::check_edit_permission', 'WPAPI-1.2', 'json_check_edit_permission' ); + + return json_check_edit_permission( $post ); + } } From efe83d67308015850998050501880a6a9e483d05 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 15:31:31 -0500 Subject: [PATCH 461/515] Update permission checks in Post controller to use new global functions. --- lib/class-wp-json-posts.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 5c33cea986..58c69c1322 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -79,7 +79,7 @@ public function get_revisions( $id ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! $this->check_edit_permission( $parent ) ) { + if ( ! json_check_edit_permission( $parent ) ) { return new WP_Error( 'json_cannot_view', __( 'Sorry, you cannot view the revisions for this post.' ), array( 'status' => 403 ) ); } @@ -619,7 +619,7 @@ protected function prepare_post( $post, $context = 'view' ) { // Don't allow unauthenticated users to read password-protected posts if ( ! empty( $post['post_password'] ) ) { - if ( ! $this->check_edit_permission( $post ) ) { + if ( ! json_check_edit_permission( $post ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 403 ) ); } From 7371321eacbed541e3e4ead3cfc5a7dcd8e791fe Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 16:07:02 -0500 Subject: [PATCH 462/515] Restructure logic in read and edit permission checks to return correct response. --- plugin.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/plugin.php b/plugin.php index af05736d95..ce563dc3bd 100644 --- a/plugin.php +++ b/plugin.php @@ -506,6 +506,8 @@ function json_ensure_response( $response ) { * @return boolean Can we read it? */ function json_check_read_permission( $object, $type = 'post' ) { + $permisson = false; + if ( 'post' == $type ) { $post_type = get_post_type_object( $object['post_type'] ); @@ -514,7 +516,6 @@ function json_check_read_permission( $object, $type = 'post' ) { return false; } - // Can we read the post? if ( 'publish' === $object['post_status'] || current_user_can( $post_type->cap->read_post, $object['ID'] ) ) { $permisson = true; } @@ -533,11 +534,9 @@ function json_check_read_permission( $object, $type = 'post' ) { if ( 'inherit' === $object['post_status'] ) { $permisson = true; } - - $permisson = false; } - return apply_filters( "json_read_{$object}_permission", $permission, $object ); + return apply_filters( "json_read_{$type}_permission", $permission, $object ); } /** @@ -548,17 +547,17 @@ function json_check_read_permission( $object, $type = 'post' ) { * @return boolean Can we edit it? */ function json_check_edit_permission( $object, $type = 'post' ) { + $permission = false; + if ( 'post' == $type ) { - $post_type = get_post_type_object( $object['post_type'] ); + $post_type = get_post_type_object( $object['post_type'] ); - if ( ! current_user_can( $post_type->cap->edit_post, $object['ID'] ) ) { + if ( current_user_can( $post_type->cap->edit_post, $object['ID'] ) ) { $permission = true; } - - $permission = false; } - return apply_filters( "json_edit_{$object}_permission", $permission, $object ); + return apply_filters( "json_edit_{$type}_permission", $permission, $object ); } /** From d650211a3c99a35a0e8d8db8d4f0d27e7179269a Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 16:08:57 -0500 Subject: [PATCH 463/515] Update read permission checks in Post controller to use global function. --- lib/class-wp-json-posts.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 58c69c1322..e205bbb215 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -185,7 +185,7 @@ public function get_posts( $filter = array(), $context = 'view', $type = 'post', $post = get_object_vars( $post ); // Do we have permission to read this post? - if ( ! $this->check_read_permission( $post ) ) { + if ( ! json_check_read_permission( $post ) ) { continue; } @@ -276,7 +276,7 @@ public function get_post( $id, $context = 'view' ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! $this->check_read_permission( $post ) ) { + if ( ! json_check_read_permission( $post ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); } @@ -445,7 +445,7 @@ public function get_comments( $id ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! $this->check_read_permission( $post ) ) { + if ( ! json_check_read_permission( $post ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); } @@ -607,7 +607,7 @@ protected function prepare_post( $post, $context = 'view' ) { $post_type = get_post_type_object( $post['post_type'] ); - if ( ! $this->check_read_permission( $post ) ) { + if ( ! json_check_read_permission( $post ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); } From fa5187066fdae64cc85126b2f52c84ff6165e213 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 16:14:54 -0500 Subject: [PATCH 464/515] Replace edit permission check in Post Meta controller with global function. --- lib/class-wp-json-meta-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-meta-posts.php b/lib/class-wp-json-meta-posts.php index a3d98a7545..7415f98504 100644 --- a/lib/class-wp-json-meta-posts.php +++ b/lib/class-wp-json-meta-posts.php @@ -46,7 +46,7 @@ protected function check_object( $id ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! $this->check_edit_permission( $post ) ) { + if ( ! json_check_edit_permission( $post ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); } From cfb3c7bee66be6c5224eab2a70f1087af8c1e5ce Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 16:15:34 -0500 Subject: [PATCH 465/515] Remove Post Meta check edit permission method in favor of global. --- lib/class-wp-json-meta-posts.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/class-wp-json-meta-posts.php b/lib/class-wp-json-meta-posts.php index 7415f98504..53875d761c 100644 --- a/lib/class-wp-json-meta-posts.php +++ b/lib/class-wp-json-meta-posts.php @@ -15,22 +15,6 @@ class WP_JSON_Meta_Posts extends WP_JSON_Meta { */ protected $type = 'post'; - /** - * Check if we can edit a post. - * - * @param array $post Post data - * @return boolean Can we edit it? - */ - protected function check_edit_permission( $post ) { - $post_type = get_post_type_object( $post['post_type'] ); - - if ( ! current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { - return false; - } - - return true; - } - /** * Check that the object can be accessed. * From 3f7d266cbef2adb56a017814084c74e1d748dd5b Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 17:13:53 -0500 Subject: [PATCH 466/515] Correct spelling of permission variable. --- plugin.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin.php b/plugin.php index ce563dc3bd..7ca63affa7 100644 --- a/plugin.php +++ b/plugin.php @@ -506,7 +506,7 @@ function json_ensure_response( $response ) { * @return boolean Can we read it? */ function json_check_read_permission( $object, $type = 'post' ) { - $permisson = false; + $permission = false; if ( 'post' == $type ) { $post_type = get_post_type_object( $object['post_type'] ); @@ -517,7 +517,7 @@ function json_check_read_permission( $object, $type = 'post' ) { } if ( 'publish' === $object['post_status'] || current_user_can( $post_type->cap->read_post, $object['ID'] ) ) { - $permisson = true; + $permission = true; } // Can we read the parent if we're inheriting? @@ -525,14 +525,14 @@ function json_check_read_permission( $object, $type = 'post' ) { $parent = get_post( $object['post_parent'], ARRAY_A ); if ( json_check_read_permission( $parent ) ) { - $permisson = true; + $permission = true; } } // If we don't have a parent, but the status is set to inherit, assume // it's published (as per get_post_status()) if ( 'inherit' === $object['post_status'] ) { - $permisson = true; + $permission = true; } } From 035d30c56a7e8e633c34dc78956f7bfe55247555 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 19:35:26 -0500 Subject: [PATCH 467/515] Replace inline edit capability checks with json_check_edit_permission(). --- lib/class-wp-json-posts.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index e205bbb215..6f779a211a 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -685,7 +685,7 @@ protected function prepare_post( $post, $context = 'view' ) { // Authorized fields // TODO: Send `Vary: Authorization` to clarify that the data can be // changed by the user's auth status - if ( current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + if ( json_check_edit_permission( $post ) ) { $post_fields_extended['password'] = $post['post_password']; } @@ -720,7 +720,7 @@ protected function prepare_post( $post, $context = 'view' ) { $_post = array_merge( $_post, $post_fields_extended ); if ( 'edit' === $context ) { - if ( current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + if ( json_check_edit_permission( $post ) ) { $_post = array_merge( $_post, $post_fields_raw ); } else { $GLOBALS['post'] = $previous_post; @@ -730,7 +730,7 @@ protected function prepare_post( $post, $context = 'view' ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); } } elseif ( 'view-revision' == $context ) { - if ( current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + if ( json_check_edit_permission( $post ) ) { $_post = array_merge( $_post, $post_fields_raw ); } else { $GLOBALS['post'] = $previous_post; @@ -854,6 +854,7 @@ protected function insert_post( $data ) { } $post_type = get_post_type_object( $current_post->post_type ); + $post['post_type'] = $current_post->post_type; } else { // Creating new post, use default type $post['post_type'] = apply_filters( 'json_insert_default_post_type', 'post' ); @@ -866,7 +867,7 @@ protected function insert_post( $data ) { // Permissions check if ( $update ) { - if ( ! current_user_can( $post_type->cap->edit_post, $data['ID'] ) ) { + if ( ! json_check_edit_permission( $post ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => 401 ) ); } From aa067588e802ccbea0573b121f49753e9f5c8dc2 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 19:36:44 -0500 Subject: [PATCH 468/515] Add json_check_create_permission function. --- plugin.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/plugin.php b/plugin.php index 7ca63affa7..d769210c8a 100644 --- a/plugin.php +++ b/plugin.php @@ -560,6 +560,27 @@ function json_check_edit_permission( $object, $type = 'post' ) { return apply_filters( "json_edit_{$type}_permission", $permission, $object ); } +/** + * Check if we have permissions to create an object. + * + * @param object $object Post|User|Term object. + * @param string $type Slug of object being passed. + * @return boolean Can we create it? + */ +function json_check_create_permission( $object, $type = 'post' ) { + $permission = false; + + if ( 'post' == $type ) { + $post_type = get_post_type_object( $object['post_type'] ); + + if ( current_user_can( $post_type->cap->create_posts ) || current_user_can( $post_type->cap->edit_posts ) ) { + $permission = true; + } + } + + return apply_filters( "json_create_{$type}_permission", $permission, $object ); +} + /** * Parse an RFC3339 timestamp into a DateTime. * From 931eef820608d7203d4be26850f36c343cb3fe4a Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 19:37:09 -0500 Subject: [PATCH 469/515] Add json_check_delete_permission function. --- plugin.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/plugin.php b/plugin.php index d769210c8a..a79e3f8903 100644 --- a/plugin.php +++ b/plugin.php @@ -581,6 +581,27 @@ function json_check_create_permission( $object, $type = 'post' ) { return apply_filters( "json_create_{$type}_permission", $permission, $object ); } +/** + * Check if we have permissions to delete an object. + * + * @param object $object Post|User|Term object. + * @param string $type Slug of object being passed. + * @return boolean Can we delete it? + */ +function json_check_delete_permission( $object, $type = 'post' ) { + $permission = false; + + if ( 'post' == $type ) { + $post_type = get_post_type_object( $object['post_type'] ); + + if ( current_user_can( $post_type->cap->delete_post, $object['ID'] ) ) { + $permission = true; + } + } + + return apply_filters( "json_delete_{$type}_permission", $permission, $object ); +} + /** * Parse an RFC3339 timestamp into a DateTime. * From a35b39f4367d09c2bb88d0d8a21a8173ff7fb961 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Wed, 10 Sep 2014 19:38:29 -0500 Subject: [PATCH 470/515] Replace inline permission checks with create and delete permission check functions. --- lib/class-wp-json-posts.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 6f779a211a..53d113f0db 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -369,9 +369,7 @@ public function delete_post( $id, $force = false ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - $post_type = get_post_type_object( $post['post_type'] ); - - if ( ! current_user_can( $post_type->cap->delete_post, $id ) ) { + if ( ! json_check_delete_permission( $post ) ) { return new WP_Error( 'json_user_cannot_delete_post', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => 401 ) ); } @@ -875,7 +873,7 @@ protected function insert_post( $data ) { return new WP_Error( 'json_cannot_change_post_type', __( 'The post type may not be changed.' ), array( 'status' => 400 ) ); } } else { - if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) { + if ( ! json_check_create_permission( $post ) ) { return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to post on this site.' ), array( 'status' => 403 ) ); } } From f3338c4353d58f6ac7c409a99f643dd18d1677ea Mon Sep 17 00:00:00 2001 From: kalenjohnson Date: Tue, 16 Sep 2014 16:54:04 -0700 Subject: [PATCH 471/515] Added taxonomy to term data list --- lib/class-wp-json-taxonomies.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index ccbe4b8b4e..457be53191 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -321,6 +321,7 @@ protected function prepare_taxonomy_term( $term, $context = 'view' ) { 'name' => $term->name, 'slug' => $term->slug, 'description' => $term->description, + 'taxonomy' => $term->taxonomy, 'parent' => (int) $term->parent, 'count' => (int) $term->count, 'link' => get_term_link( $term, $term->taxonomy ), From ef44a2d5988ef3973ae62f8d37ecd81c144b586f Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 19 Sep 2014 12:20:59 -0500 Subject: [PATCH 472/515] Change to object based permission checks for more flexibility. Create new function json_check_post_permission. Remove previous json_check_{capability}_permission functions. Update permission check function names in Posts and Meta routes. --- lib/class-wp-json-meta-posts.php | 4 +- lib/class-wp-json-posts.php | 40 +++++----- plugin.php | 132 +++++++++++-------------------- 3 files changed, 68 insertions(+), 108 deletions(-) diff --git a/lib/class-wp-json-meta-posts.php b/lib/class-wp-json-meta-posts.php index 53875d761c..24df5ca21e 100644 --- a/lib/class-wp-json-meta-posts.php +++ b/lib/class-wp-json-meta-posts.php @@ -30,7 +30,7 @@ protected function check_object( $id ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! json_check_edit_permission( $post ) ) { + if ( ! json_check_post_permission( $post, 'edit' ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); } @@ -125,4 +125,4 @@ public function insert_post_meta( $post, $data ) { public function _deprecated_call( $method, $args ) { return call_user_func_array( array( $this, $method ), $args ); } -} \ No newline at end of file +} diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 53d113f0db..58c2e9d35c 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -79,7 +79,7 @@ public function get_revisions( $id ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! json_check_edit_permission( $parent ) ) { + if ( ! json_check_post_permission( $parent, 'edit' ) ) { return new WP_Error( 'json_cannot_view', __( 'Sorry, you cannot view the revisions for this post.' ), array( 'status' => 403 ) ); } @@ -185,7 +185,7 @@ public function get_posts( $filter = array(), $context = 'view', $type = 'post', $post = get_object_vars( $post ); // Do we have permission to read this post? - if ( ! json_check_read_permission( $post ) ) { + if ( ! json_check_post_permission( $post, 'read' ) ) { continue; } @@ -276,7 +276,7 @@ public function get_post( $id, $context = 'view' ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! json_check_read_permission( $post ) ) { + if ( ! json_check_post_permission( $post, 'read' ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); } @@ -369,7 +369,7 @@ public function delete_post( $id, $force = false ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! json_check_delete_permission( $post ) ) { + if ( ! json_check_post_permission( $post, 'delete' ) ) { return new WP_Error( 'json_user_cannot_delete_post', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => 401 ) ); } @@ -443,7 +443,7 @@ public function get_comments( $id ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! json_check_read_permission( $post ) ) { + if ( ! json_check_post_permission( $post, 'read' ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); } @@ -605,7 +605,7 @@ protected function prepare_post( $post, $context = 'view' ) { $post_type = get_post_type_object( $post['post_type'] ); - if ( ! json_check_read_permission( $post ) ) { + if ( ! json_check_post_permission( $post, 'read' ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); } @@ -617,7 +617,7 @@ protected function prepare_post( $post, $context = 'view' ) { // Don't allow unauthenticated users to read password-protected posts if ( ! empty( $post['post_password'] ) ) { - if ( ! json_check_edit_permission( $post ) ) { + if ( ! json_check_post_permission( $post, 'edit' ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 403 ) ); } @@ -683,7 +683,7 @@ protected function prepare_post( $post, $context = 'view' ) { // Authorized fields // TODO: Send `Vary: Authorization` to clarify that the data can be // changed by the user's auth status - if ( json_check_edit_permission( $post ) ) { + if ( json_check_post_permission( $post, 'edit' ) ) { $post_fields_extended['password'] = $post['post_password']; } @@ -718,7 +718,7 @@ protected function prepare_post( $post, $context = 'view' ) { $_post = array_merge( $_post, $post_fields_extended ); if ( 'edit' === $context ) { - if ( json_check_edit_permission( $post ) ) { + if ( json_check_post_permission( $post, 'edit' ) ) { $_post = array_merge( $_post, $post_fields_raw ); } else { $GLOBALS['post'] = $previous_post; @@ -728,7 +728,7 @@ protected function prepare_post( $post, $context = 'view' ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you cannot edit this post' ), array( 'status' => 403 ) ); } } elseif ( 'view-revision' == $context ) { - if ( json_check_edit_permission( $post ) ) { + if ( json_check_post_permission( $post, 'edit' ) ) { $_post = array_merge( $_post, $post_fields_raw ); } else { $GLOBALS['post'] = $previous_post; @@ -865,7 +865,7 @@ protected function insert_post( $data ) { // Permissions check if ( $update ) { - if ( ! json_check_edit_permission( $post ) ) { + if ( ! json_check_post_permission( $post, 'edit' ) ) { return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => 401 ) ); } @@ -873,7 +873,7 @@ protected function insert_post( $data ) { return new WP_Error( 'json_cannot_change_post_type', __( 'The post type may not be changed.' ), array( 'status' => 400 ) ); } } else { - if ( ! json_check_create_permission( $post ) ) { + if ( ! json_check_post_permission( $post, 'create' ) ) { return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to post on this site.' ), array( 'status' => 403 ) ); } } @@ -887,13 +887,13 @@ protected function insert_post( $data ) { case 'pending': break; case 'private': - if ( ! current_user_can( $post_type->cap->publish_posts ) ) { + if ( ! json_check_post_permission( $post, 'publish_posts' ) ) { return new WP_Error( 'json_cannot_create_private', __( 'Sorry, you are not allowed to create private posts in this post type' ), array( 'status' => 403 ) ); } break; case 'publish': case 'future': - if ( ! current_user_can( $post_type->cap->publish_posts ) ) { + if ( ! json_check_post_permission( $post, 'publish_posts' ) ) { return new WP_Error( 'json_cannot_publish', __( 'Sorry, you are not allowed to publish posts in this post type' ), array( 'status' => 403 ) ); } break; @@ -944,7 +944,7 @@ protected function insert_post( $data ) { // Only check edit others' posts if we are another user if ( $data['author'] !== get_current_user_id() ) { - if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) { + if ( ! json_check_post_permission( $post, 'edit_others_posts' ) ) { return new WP_Error( 'json_cannot_edit_others', __( 'You are not allowed to edit posts as this user.' ), array( 'status' => 401 ) ); } @@ -962,7 +962,7 @@ protected function insert_post( $data ) { if ( ! empty( $data['password'] ) ) { $post['post_password'] = $data['password']; - if ( ! current_user_can( $post_type->cap->publish_posts ) ) { + if ( ! json_check_post_permission( $post, 'publish_posts' ) ) { return new WP_Error( 'json_cannot_create_passworded', __( 'Sorry, you are not allowed to create password protected posts in this post type' ), array( 'status' => 401 ) ); } } @@ -1295,9 +1295,9 @@ protected function is_valid_meta_data( $data ) { * @return boolean Can we read it? */ protected function check_read_permission( $post ) { - _deprecated_function( 'WP_JSON_Posts::check_read_permission', 'WPAPI-1.2', 'json_check_read_permission' ); + _deprecated_function( 'WP_JSON_Posts::check_read_permission', 'WPAPI-1.2', 'json_check_post_permission' ); - return json_check_read_permission( $post ); + return json_check_post_permission( $post, 'read' ); } /** @@ -1309,8 +1309,8 @@ protected function check_read_permission( $post ) { * @return boolean Can we edit it? */ protected function check_edit_permission( $post ) { - _deprecated_function( 'WP_JSON_Posts::check_edit_permission', 'WPAPI-1.2', 'json_check_edit_permission' ); + _deprecated_function( 'WP_JSON_Posts::check_edit_permission', 'WPAPI-1.2', 'json_check_post_permission' ); - return json_check_edit_permission( $post ); + return json_check_post_permission( $post, 'edit' ); } } diff --git a/plugin.php b/plugin.php index a79e3f8903..e1c09ab95d 100644 --- a/plugin.php +++ b/plugin.php @@ -499,107 +499,67 @@ function json_ensure_response( $response ) { } /** - * Check if we have permissions to read an object. + * Check if we have permission to interact with the post object. * - * @param object $object Post|User|Term object. - * @param string $type Slug of object being passed. - * @return boolean Can we read it? + * @param WP_Post $post Post object. + * @param string $capability Permission to check. + * @return boolean Can we interact with it? */ -function json_check_read_permission( $object, $type = 'post' ) { +function json_check_post_permission( $post, $capability = 'read' ) { $permission = false; + $post_type = get_post_type_object( $post['post_type'] ); - if ( 'post' == $type ) { - $post_type = get_post_type_object( $object['post_type'] ); - - // Ensure the post type can be read - if ( ! $post_type->show_in_json ) { - return false; - } - - if ( 'publish' === $object['post_status'] || current_user_can( $post_type->cap->read_post, $object['ID'] ) ) { - $permission = true; - } - - // Can we read the parent if we're inheriting? - if ( 'inherit' === $object['post_status'] && $object['post_parent'] > 0 ) { - $parent = get_post( $object['post_parent'], ARRAY_A ); + switch ( $capability ) { + case 'read' : + if ( ! $post_type->show_in_json ) { + return false; + } - if ( json_check_read_permission( $parent ) ) { + if ( 'publish' === $post['post_status'] || current_user_can( $post_type->cap->read_post, $post['ID'] ) ) { $permission = true; } - } - - // If we don't have a parent, but the status is set to inherit, assume - // it's published (as per get_post_status()) - if ( 'inherit' === $object['post_status'] ) { - $permission = true; - } - } - - return apply_filters( "json_read_{$type}_permission", $permission, $object ); -} -/** - * Check if we have permissions to edit an object. - * - * @param object $object Post|User|Term object. - * @param string $type Slug of object being passed. - * @return boolean Can we edit it? - */ -function json_check_edit_permission( $object, $type = 'post' ) { - $permission = false; - - if ( 'post' == $type ) { - $post_type = get_post_type_object( $object['post_type'] ); - - if ( current_user_can( $post_type->cap->edit_post, $object['ID'] ) ) { - $permission = true; - } - } - - return apply_filters( "json_edit_{$type}_permission", $permission, $object ); -} + // Can we read the parent if we're inheriting? + if ( 'inherit' === $post['post_status'] && $post['post_parent'] > 0 ) { + $parent = get_post( $post['post_parent'], ARRAY_A ); -/** - * Check if we have permissions to create an object. - * - * @param object $object Post|User|Term object. - * @param string $type Slug of object being passed. - * @return boolean Can we create it? - */ -function json_check_create_permission( $object, $type = 'post' ) { - $permission = false; - - if ( 'post' == $type ) { - $post_type = get_post_type_object( $object['post_type'] ); + if ( json_check_post_permission( $parent, 'read' ) ) { + $permission = true; + } + } - if ( current_user_can( $post_type->cap->create_posts ) || current_user_can( $post_type->cap->edit_posts ) ) { - $permission = true; - } - } + // If we don't have a parent, but the status is set to inherit, assume + // it's published (as per get_post_status()) + if ( 'inherit' === $post['post_status'] ) { + $permission = true; + } + break; - return apply_filters( "json_create_{$type}_permission", $permission, $object ); -} + case 'edit' : + if ( current_user_can( $post_type->cap->edit_post, $post['ID'] ) ) { + $permission = true; + } + break; -/** - * Check if we have permissions to delete an object. - * - * @param object $object Post|User|Term object. - * @param string $type Slug of object being passed. - * @return boolean Can we delete it? - */ -function json_check_delete_permission( $object, $type = 'post' ) { - $permission = false; + case 'create' : + if ( current_user_can( $post_type->cap->create_posts ) || current_user_can( $post_type->cap->edit_posts ) ) { + $permission = true; + } + break; - if ( 'post' == $type ) { - $post_type = get_post_type_object( $object['post_type'] ); + case 'delete' : + if ( current_user_can( $post_type->cap->delete_post, $post['ID'] ) ) { + $permission = true; + } + break; - if ( current_user_can( $post_type->cap->delete_post, $object['ID'] ) ) { - $permission = true; - } + default : + if ( current_user_can( $post_type->cap->$capability ) ) { + $permission = true; + } } - return apply_filters( "json_delete_{$type}_permission", $permission, $object ); + return apply_filters( "json_check_post_{$capability}_permission", $permission, $post ); } /** @@ -650,7 +610,7 @@ function json_get_date_with_gmt( $date, $force_utc = false ) { * Explicitly strips timezones, as datetimes are not saved with any timezone * information. Including any information on the offset could be misleading. * - * @param string $date + * @param string $date */ function json_mysql_to_rfc3339( $date_string ) { $formatted = mysql2date( 'c', $date_string, false ); From 33ea6d3a4d55738ff55d1403469b8b16302fe05d Mon Sep 17 00:00:00 2001 From: Justin Sternberg Date: Wed, 24 Sep 2014 13:53:34 -0400 Subject: [PATCH 473/515] account for possible query strings in our dispatch method --- lib/class-wp-json-server.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index fa8105a98f..a19c10fb29 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -395,12 +395,20 @@ public function dispatch() { } if ( $supported & self::ACCEPT_JSON ) { - $data = json_decode( $this->get_raw_data(), true ); + $raw_data = $this->get_raw_data(); + $data = json_decode( $raw_data, true ); // test for json_decode() error $json_error_message = $this->get_json_last_error(); if ( $json_error_message ) { - return new WP_Error( 'json_decode_error', $json_error_message, array( 'status' => 500 ) ); + + $data = array(); + parse_str( $raw_data, $data ); + + if ( empty( $data ) ) { + + return new WP_Error( 'json_decode_error', $json_error_message, array( 'status' => 500 ) ); + } } if ( $data !== null ) { From 4df0bc0249c3c2ae914ab85617f85d549d486570 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Thu, 25 Sep 2014 16:09:56 -0500 Subject: [PATCH 474/515] Applied fix from 9d4af6 to get parent of comment. --- lib/class-wp-json-comments.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-comments.php b/lib/class-wp-json-comments.php index 06765fee56..75f8b0f2ee 100644 --- a/lib/class-wp-json-comments.php +++ b/lib/class-wp-json-comments.php @@ -82,7 +82,7 @@ public function get_comments( $id ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! $this->check_read_permission( $post ) ) { + if ( ! json_check_post_permission( $post, 'read' ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); } @@ -166,7 +166,7 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen if ( $context === 'single' ) { $parent_fields[] = 'comment'; } - $parent = get_comment( $post['post_parent'] ); + $parent = get_comment( $comment->comment_parent ); $fields['parent'] = $this->prepare_comment( $parent, $parent_fields, 'single-parent' ); } From ebcc8e3f8fca51fd479e0b6566197e3614282262 Mon Sep 17 00:00:00 2001 From: Japh Date: Mon, 29 Sep 2014 14:58:47 +0300 Subject: [PATCH 475/515] Updated reference to Chassis repo --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fa7425928c..76a38c98d3 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,10 @@ Want to test out WP-API and work on it? Here's how you can set up your own testing environment in a few easy steps: 1. Install [Vagrant](http://vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/). -2. Clone [Chassis](https://github.com/sennza/Chassis): +2. Clone [Chassis](https://github.com/Chassis/Chassis): ```bash - git clone --recursive git@github.com:sennza/Chassis.git api-tester + git clone --recursive git@github.com:Chassis/Chassis.git api-tester ``` 3. Grab a copy of WP API: @@ -104,13 +104,13 @@ For testing, you'll need a little bit more: # From your base directory, api-tester if following the steps from before git clone --recursive https://github.com/Chassis/Tester.git extensions/tester ``` - + 2. Run the provisioner: ``` vagrant provision ``` - + 3. Log in to the virtual machine and run the testing suite: ```bash From 87f20d0725197296ca9db7375328358e01f31ca1 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 6 Oct 2014 13:49:30 -0500 Subject: [PATCH 476/515] Reverting the removal of timezone handling mentioned in #448. - Adds date_tz and modified_tz fields back to post response. - Adds date_tz field back to comment response. --- lib/class-wp-json-posts.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 09846e0f82..35692dab74 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -717,21 +717,29 @@ protected function prepare_post( $post, $context = 'view' ) { ); // Dates + $timezone = json_get_timezone(); + if ( $post['post_date_gmt'] === '0000-00-00 00:00:00' ) { - $post_fields['date'] = null; + $post_fields['date'] = null; + $post_fields_extended['date_tz'] = null; $post_fields_extended['date_gmt'] = null; } else { + $post_date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_date'], $timezone ); $post_fields['date'] = json_mysql_to_rfc3339( $post['post_date'] ); + $post_fields_extended['date_tz'] = $post_date->format( 'e' ); $post_fields_extended['date_gmt'] = json_mysql_to_rfc3339( $post['post_date_gmt'] ); } if ( $post['post_modified_gmt'] === '0000-00-00 00:00:00' ) { - $post_fields['modified'] = null; + $post_fields['modified'] = null; + $post_fields_extended['modified_tz'] = null; $post_fields_extended['modified_gmt'] = null; } else { + $modified_date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_modified'], $timezone ); $post_fields['modified'] = json_mysql_to_rfc3339( $post['post_modified'] ); + $post_fields_extended['modified_tz'] = $modified_date->format( 'e' ); $post_fields_extended['modified_gmt'] = json_mysql_to_rfc3339( $post['post_modified_gmt'] ); } @@ -1173,7 +1181,11 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen } // Date + $timezone = json_get_timezone(); + + $comment_date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $comment->comment_date, $timezone ); $fields['date'] = json_mysql_to_rfc3339( $comment->comment_date ); + $fields['date_tz'] = $comment_date->format( 'e' ); $fields['date_gmt'] = json_mysql_to_rfc3339( $comment->comment_date_gmt ); // Meta From b5074b822005426d958868b9a480a2fd2809b881 Mon Sep 17 00:00:00 2001 From: NikV Date: Tue, 7 Oct 2014 21:46:59 -0500 Subject: [PATCH 477/515] Making small update to go directly to Getting Started Guide The Link to getting started guide led to the regular guides link, I made it go directly to the Getting Started Guide. --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 62eab164df..ff989fd3e3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,7 +16,7 @@ Dive in deeper into the [schema details][schema] to better understand the little details, or read about the [philosophy][] behind them. Read about the [implementation details][implementation] on how the API works internally. -[Getting Started]: http://wp-api.org/guides.html#getting-started +[Getting Started]: http://wp-api.org/guides/getting-started.html [guides]: http://wp-api.org/guides.html [post-routes]: http://wp-api.org/#posts [media-routes]: http://wp-api.org/#media From 21fa90c56cd4018f48b647ccdba53ca460289c8a Mon Sep 17 00:00:00 2001 From: "Wayne K. Walrath" Date: Thu, 9 Oct 2014 17:06:33 -0400 Subject: [PATCH 478/515] WP_JSON_Posts::get_post_types() should return array, not hash/object --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 09846e0f82..1324ca72c2 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -546,7 +546,7 @@ public function get_post_types() { continue; } - $types[ $name ] = $type; + $types[] = $type; } return $types; From 2b88983268dcfa1f7998c6979a711c6bbe618aa1 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Wed, 15 Oct 2014 12:23:24 -0700 Subject: [PATCH 479/515] `prepare_post()` can return `WP_Error` Catch before we try to use it as an array --- lib/class-wp-json-customposttype.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/class-wp-json-customposttype.php b/lib/class-wp-json-customposttype.php index 997fee7248..02fd1a1d00 100644 --- a/lib/class-wp-json-customposttype.php +++ b/lib/class-wp-json-customposttype.php @@ -194,6 +194,9 @@ public function delete_post( $id, $force = false ) { */ protected function prepare_post( $post, $context = 'view' ) { $_post = parent::prepare_post( $post, $context ); + if ( is_wp_error( $_post ) ) { + return $_post; + } // Override entity meta keys with the correct links $_post['meta'] = array( From c593bd5401d44d970dea75689bdbc92bfe61bf86 Mon Sep 17 00:00:00 2001 From: paul de wouters Date: Mon, 10 Nov 2014 10:07:42 -0300 Subject: [PATCH 480/515] Fixes some PHP docblocks Also type hints the $data parameter --- lib/class-wp-json-pages.php | 14 ++++++++++++-- lib/class-wp-json-posts.php | 9 ++++----- plugin.php | 4 +++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/class-wp-json-pages.php b/lib/class-wp-json-pages.php index 846ca697fb..e460113f9d 100644 --- a/lib/class-wp-json-pages.php +++ b/lib/class-wp-json-pages.php @@ -58,6 +58,9 @@ public function register_routes( $routes ) { * Retrieve a page by path name * * @param string $path + * @param string $context + * + * @return array|WP_Error */ public function get_post_by_path( $path, $context = 'view' ) { $post = get_page_by_path( $path, ARRAY_A ); @@ -72,7 +75,11 @@ public function get_post_by_path( $path, $context = 'view' ) { /** * Edit a page by path name * - * @param string $path + * @param $path + * @param $data + * @param array $_headers + * + * @return true|WP_Error */ public function edit_post_by_path( $path, $data, $_headers = array() ) { $post = get_page_by_path( $path, ARRAY_A ); @@ -87,7 +94,10 @@ public function edit_post_by_path( $path, $data, $_headers = array() ) { /** * Delete a page by path name * - * @param string $path + * @param $path + * @param bool $force + * + * @return true|WP_Error */ public function delete_post_by_path( $path, $force = false ) { $post = get_page_by_path( $path, ARRAY_A ); diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 05c9ad0463..4a705da2bd 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -714,13 +714,12 @@ public function add_post_type_data( $data, $taxonomy, $context = 'view' ) { /** * Helper method for {@see create_post} and {@see edit_post}, containing shared logic. * - * @since 3.4.0 - * @uses wp_insert_post() * - * @param WP_User $user The post author if post_author isn't set in $content_struct. - * @param array $content_struct Post data to insert. + * @param array $data Post data to insert. + * + * @return int|WP_Error */ - protected function insert_post( $data ) { + protected function insert_post( array $data ) { $post = array(); $update = ! empty( $data['ID'] ); diff --git a/plugin.php b/plugin.php index a84b6302e8..a22f8f6ad2 100644 --- a/plugin.php +++ b/plugin.php @@ -611,7 +611,9 @@ function json_get_date_with_gmt( $date, $force_utc = false ) { * Explicitly strips timezones, as datetimes are not saved with any timezone * information. Including any information on the offset could be misleading. * - * @param string $date + * @param string $date_string + * + * @return mixed */ function json_mysql_to_rfc3339( $date_string ) { $formatted = mysql2date( 'c', $date_string, false ); From 22609227418c8b101439bb4e0e1fa923e3767cba Mon Sep 17 00:00:00 2001 From: Sarah Gooding Date: Sun, 9 Nov 2014 00:37:33 -0600 Subject: [PATCH 481/515] Update routes.md posts_per_page is actually a public query variable --- docs/routes/routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/routes/routes.md b/docs/routes/routes.md index abb73df52c..ede70221f4 100644 --- a/docs/routes/routes.md +++ b/docs/routes/routes.md @@ -147,12 +147,12 @@ The following query variables are available to the API: * `term` * `cpage` * `post_type` +* `posts_per_page` In addition, the following are available when authenticated as a user with `edit_posts`: * `offset` -* `posts_per_page` * `posts_per_archive_page` * `showposts` * `nopaging` From 43f66a5423785e25e75e517496fd7f6242b941b9 Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Fri, 19 Dec 2014 17:14:52 -0500 Subject: [PATCH 482/515] Merge JS into main repo --- plugin.php | 2 +- wp-api.js | 987 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 988 insertions(+), 1 deletion(-) create mode 100644 wp-api.js diff --git a/plugin.php b/plugin.php index a22f8f6ad2..b56a8eacba 100644 --- a/plugin.php +++ b/plugin.php @@ -238,7 +238,7 @@ function json_api_deactivation( $network_wide ) { * @see wp_register_scripts() */ function json_register_scripts() { - wp_register_script( 'wp-api', 'http://wp-api.github.io/client-js/build/js/wp-api.js', array( 'jquery', 'backbone', 'underscore' ), '1.1', true ); + wp_register_script( 'wp-api', esc_url_raw( plugins_url( 'wp-api.js', __FILE__ ) ), array( 'jquery', 'backbone', 'underscore' ), '1.1', true ); $settings = array( 'root' => esc_url_raw( get_json_url() ), 'nonce' => wp_create_nonce( 'wp_json' ) ); wp_localize_script( 'wp-api', 'WP_API_Settings', $settings ); diff --git a/wp-api.js b/wp-api.js new file mode 100644 index 0000000000..56e70c2bf2 --- /dev/null +++ b/wp-api.js @@ -0,0 +1,987 @@ +(function( window, undefined ) { + + 'use strict'; + + function WP_API() { + this.models = {}; + this.collections = {}; + this.views = {}; + } + + window.wp = window.wp || {}; + wp.api = wp.api || new WP_API(); + +})( window ); + +(function( Backbone, _, window, undefined ) { + + //'use strict'; + + // ECMAScript 5 shim, from MDN + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString + if ( ! Date.prototype.toISOString ) { + var pad = function( number ) { + var r = String( number ); + if ( r.length === 1 ) { + r = '0' + r; + } + return r; + }; + + Date.prototype.toISOString = function() { + return this.getUTCFullYear() + + '-' + pad( this.getUTCMonth() + 1 ) + + '-' + pad( this.getUTCDate() ) + + 'T' + pad( this.getUTCHours() ) + + ':' + pad( this.getUTCMinutes() ) + + ':' + pad( this.getUTCSeconds() ) + + '.' + String( ( this.getUTCMilliseconds()/1000 ).toFixed( 3 ) ).slice( 2, 5 ) + + 'Z'; + }; + } + + function WP_API_Utils() { + var origParse = Date.parse, + numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; + + + this.parseISO8601 = function( date ) { + var timestamp, struct, i, k, + minutesOffset = 0; + + // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string + // before falling back to any implementation-specific date parsing, so that’s what we do, even if native + // implementations could be faster + // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm + 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))) { + // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC + for ( i = 0; ( k = numericKeys[i] ); ++i) { + struct[k] = +struct[k] || 0; + } + + // allow undefined days and months + struct[2] = ( +struct[2] || 1 ) - 1; + struct[3] = +struct[3] || 1; + + if ( struct[8] !== 'Z' && struct[9] !== undefined ) { + minutesOffset = struct[10] * 60 + struct[11]; + + if ( struct[9] === '+' ) { + minutesOffset = 0 - minutesOffset; + } + } + + timestamp = Date.UTC( struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7] ); + } + else { + timestamp = origParse ? origParse( date ) : NaN; + } + + return timestamp; + }; + } + + window.wp = window.wp || {}; + wp.api = wp.api || {}; + wp.api.utils = wp.api.utils || new WP_API_Utils(); + +})( Backbone, _, window ); + +/* global WP_API_Settings:false */ +// Suppress warning about parse function's unused "options" argument: +/* jshint unused:false */ +(function( wp, WP_API_Settings, Backbone, _, window, undefined ) { + + 'use strict'; + + /** + * Array of parseable dates + * + * @type {string[]} + */ + var parseable_dates = [ 'date', 'modified', 'date_gmt', 'modified_gmt' ]; + + /** + * Mixin for all content that is time stamped + * + * @type {{toJSON: toJSON, parse: parse}} + */ + var TimeStampedMixin = { + /** + * Serialize the entity pre-sync + * + * @returns {*} + */ + toJSON: function() { + var attributes = _.clone( this.attributes ); + + // Serialize Date objects back into 8601 strings + _.each( parseable_dates, function ( key ) { + if ( key in attributes ) { + attributes[key] = attributes[key].toISOString(); + } + }); + + return attributes; + }, + + /** + * Unserialize the fetched response + * + * @param {*} response + * @returns {*} + */ + parse: function( response ) { + // Parse dates into native Date objects + _.each( parseable_dates, function ( key ) { + if ( ! ( key in response ) ) { + return; + } + + var timestamp = wp.api.utils.parseISO8601( response[key] ); + response[key] = new Date( timestamp ); + }); + + // Parse the author into a User object + if ( response.author !== 'undefined' ) { + response.author = new wp.api.models.User( response.author ); + } + + return response; + } + }; + + /** + * Mixin for all hierarchical content types such as posts + * + * @type {{parent: parent}} + */ + var HierarchicalMixin = { + /** + * Get parent object + * + * @returns {Backbone.Model} + */ + parent: function() { + + var object, parent = this.get( 'parent' ); + + // Return null if we don't have a parent + if ( parent === 0 ) { + return null; + } + + var parentModel = this; + + if ( typeof this.parentModel !== 'undefined' ) { + /** + * Probably a better way to do this. Perhaps grab a cached version of the + * instantiated model? + */ + parentModel = new this.parentModel(); + } + + // Can we get this from its collection? + if ( parentModel.collection ) { + return parentModel.collection.get( parent ); + } else { + // Otherwise, get the object directly + object = new parentModel.constructor( { + ID: parent + }); + + // Note that this acts asynchronously + object.fetch(); + return object; + } + } + }; + + /** + * Private Backbone base model for all models + */ + var BaseModel = Backbone.Model.extend( + /** @lends BaseModel.prototype */ + { + /** + * Set nonce header before every Backbone sync + * + * @param {string} method + * @param {Backbone.Model} model + * @param {{beforeSend}, *} options + * @returns {*} + */ + sync: function( method, model, options ) { + options = options || {}; + + if ( typeof WP_API_Settings.nonce !== 'undefined' ) { + var beforeSend = options.beforeSend; + + options.beforeSend = function( xhr ) { + xhr.setRequestHeader( 'X-WP-Nonce', WP_API_Settings.nonce ); + + if ( beforeSend ) { + return beforeSend.apply( this, arguments ); + } + }; + } + + return Backbone.sync( method, model, options ); + } + } + ); + + /** + * Backbone model for single users + */ + wp.api.models.User = BaseModel.extend( + /** @lends User.prototype */ + { + idAttribute: 'ID', + + urlRoot: WP_API_Settings.root + '/users', + + defaults: { + ID: null, + username: '', + email: '', + password: '', + name: '', + first_name: '', + last_name: '', + nickname: '', + slug: '', + URL: '', + avatar: '', + meta: { + links: {} + } + }, + + /** + * Return avatar URL + * + * @param {number} size + * @returns {string} + */ + avatar: function( size ) { + return this.get( 'avatar' ) + '&s=' + size; + } + } + ); + + /** + * Model for Taxonomy + */ + wp.api.models.Taxonomy = BaseModel.extend( + /** @lends Taxonomy.prototype */ + { + idAttribute: 'slug', + + urlRoot: WP_API_Settings.root + '/taxonomies', + + defaults: { + name: '', + slug: null, + labels: {}, + types: {}, + show_cloud: false, + hierarchical: false, + meta: { + links: {} + } + } + } + ); + + /** + * Backbone model for term + */ + wp.api.models.Term = BaseModel.extend( _.extend( + /** @lends Term.prototype */ + { + idAttribute: 'ID', + + taxonomy: 'category', + + /** + * @class Represent a term + * @augments Backbone.Model + * @constructs + */ + initialize: function( attributes, options ) { + if ( typeof options !== 'undefined' ) { + if ( options.taxonomy ) { + this.taxonomy = options.taxonomy; + } + } + }, + + /** + * Return URL for the model + * + * @returns {string} + */ + url: function() { + var id = this.get( 'ID' ); + id = id || ''; + + return WP_API_Settings.root + '/taxonomies/' + this.taxonomy + '/terms/' + id; + }, + + defaults: { + ID: null, + name: '', + slug: '', + description: '', + parent: null, + count: 0, + link: '', + meta: { + links: {} + } + } + + }, TimeStampedMixin, HierarchicalMixin ) + ); + + /** + * Backbone model for single posts + */ + wp.api.models.Post = BaseModel.extend( _.extend( + /** @lends Post.prototype */ + { + idAttribute: 'ID', + + urlRoot: WP_API_Settings.root + '/posts', + + defaults: { + ID: null, + title: '', + status: 'draft', + type: 'post', + author: new wp.api.models.User(), + content: '', + link: '', + 'parent': 0, + date: new Date(), + date_gmt: new Date(), + modified: new Date(), + modified_gmt: new Date(), + format: 'standard', + slug: '', + guid: '', + excerpt: '', + menu_order: 0, + comment_status: 'open', + ping_status: 'open', + sticky: false, + date_tz: 'Etc/UTC', + modified_tz: 'Etc/UTC', + featured_image: null, + terms: {}, + post_meta: {}, + meta: { + links: {} + } + } + }, TimeStampedMixin, HierarchicalMixin ) + ); + + /** + * Backbone model for pages + */ + wp.api.models.Page = BaseModel.extend( _.extend( + /** @lends Page.prototype */ + { + idAttribute: 'ID', + + urlRoot: WP_API_Settings.root + '/pages', + + defaults: { + ID: null, + title: '', + status: 'draft', + type: 'page', + author: new wp.api.models.User(), + content: '', + parent: 0, + link: '', + date: new Date(), + modified: new Date(), + date_gmt: new Date(), + modified_gmt: new Date(), + date_tz: 'Etc/UTC', + modified_tz: 'Etc/UTC', + format: 'standard', + slug: '', + guid: '', + excerpt: '', + menu_order: 0, + comment_status: 'closed', + ping_status: 'open', + sticky: false, + password: '', + meta: { + links: {} + }, + featured_image: null, + terms: [] + } + }, TimeStampedMixin, HierarchicalMixin ) + ); + + /** + * Backbone model for revisions + */ + wp.api.models.Revision = wp.api.models.Post.extend( + /** @lends Revision.prototype */ + { + /** + * Return URL for model + * + * @returns {string} + */ + url: function() { + var parent_id = this.get( 'parent' ); + parent_id = parent_id || ''; + + var id = this.get( 'ID' ); + id = id || ''; + + return WP_API_Settings.root + '/posts/' + parent_id + '/revisions/' + id; + }, + + /** + * @class Represent a revision + * @augments Backbone.Model + * @constructs + */ + initialize: function() { + // Todo: what of the parent model is a page? + this.parentModel = wp.api.models.Post; + } + } + ); + + /** + * Backbone model for media items + */ + wp.api.models.Media = BaseModel.extend( _.extend( + /** @lends Media.prototype */ + { + idAttribute: 'ID', + + urlRoot: WP_API_Settings.root + '/media', + + defaults: { + ID: null, + title: '', + status: 'inherit', + type: 'attachment', + author: new wp.api.models.User(), + content: '', + parent: 0, + link: '', + date: new Date(), + modified: new Date(), + format: 'standard', + slug: '', + guid: '', + excerpt: '', + menu_order: 0, + comment_status: 'open', + ping_status: 'open', + sticky: false, + date_tz: 'Etc/UTC', + modified_tz: 'Etc/UTC', + date_gmt: new Date(), + modified_gmt: new Date(), + meta: { + links: {} + }, + terms: [], + source: '', + is_image: true, + attachment_meta: {}, + image_meta: {} + }, + + /** + * @class Represent a media item + * @augments Backbone.Model + * @constructs + */ + initialize: function() { + // Todo: what of the parent model is a page? + this.parentModel = wp.api.models.Post; + } + }, TimeStampedMixin, HierarchicalMixin ) + ); + + /** + * Backbone model for comments + */ + wp.api.models.Comment = BaseModel.extend( _.extend( + /** @lends Comment.prototype */ + { + idAttribute: 'ID', + + defaults: { + ID: null, + post: null, + content: '', + status: 'hold', + type: '', + parent: 0, + author: new wp.api.models.User(), + date: new Date(), + date_gmt: new Date(), + date_tz: 'Etc/UTC', + meta: { + links: {} + } + }, + + /** + * Return URL for model + * + * @returns {string} + */ + url: function() { + var post_id = this.get( 'post' ); + post_id = post_id || ''; + + var id = this.get( 'ID' ); + id = id || ''; + + return WP_API_Settings.root + '/posts/' + post_id + '/comments/' + id; + } + }, TimeStampedMixin, HierarchicalMixin ) + ); + + /** + * Backbone model for single post types + */ + wp.api.models.PostType = BaseModel.extend( + /** @lends PostType.prototype */ + { + idAttribute: 'slug', + + urlRoot: WP_API_Settings.root + '/posts/types', + + defaults: { + slug: null, + name: '', + description: '', + labels: {}, + queryable: false, + searchable: false, + hierarchical: false, + meta: { + links: {} + }, + taxonomies: [] + }, + + /** + * Prevent model from being saved + * + * @returns {boolean} + */ + save: function () { + return false; + }, + + /** + * Prevent model from being deleted + * + * @returns {boolean} + */ + 'delete': function () { + return false; + } + } + ); + + /** + * Backbone model for a post status + */ + wp.api.models.PostStatus = BaseModel.extend( + /** @lends PostStatus.prototype */ + { + idAttribute: 'slug', + + urlRoot: WP_API_Settings.root + '/posts/statuses', + + defaults: { + slug: null, + name: '', + 'public': true, + 'protected': false, + 'private': false, + queryable: true, + show_in_list: true, + meta: { + links: {} + } + }, + + /** + * Prevent model from being saved + * + * @returns {boolean} + */ + save: function() { + return false; + }, + + /** + * Prevent model from being deleted + * + * @returns {boolean} + */ + 'delete': function() { + return false; + } + } + ); + +})( wp, WP_API_Settings, Backbone, _, window ); + +/* global WP_API_Settings:false */ +(function( wp, WP_API_Settings, Backbone, _, window, undefined ) { + + 'use strict'; + + var BaseCollection = Backbone.Collection.extend( + /** @lends BaseCollection.prototype */ + { + + /** + * Setup default state + */ + initialize: function() { + this.state = { + data: {}, + currentPage: null, + totalPages: null, + totalObjects: null + }; + }, + + /** + * Overwrite Backbone.Collection.sync to pagination state based on response headers. + * + * Set nonce header before every Backbone sync. + * + * @param {string} method + * @param {Backbone.Model} model + * @param {{success}, *} options + * @returns {*} + */ + sync: function( method, model, options ) { + options = options || {}; + var beforeSend = options.beforeSend; + + if ( typeof WP_API_Settings.nonce !== 'undefined' ) { + options.beforeSend = function( xhr ) { + xhr.setRequestHeader( 'X-WP-Nonce', WP_API_Settings.nonce ); + + if ( beforeSend ) { + return beforeSend.apply( this, arguments ); + } + }; + } + + if ( 'read' === method ) { + var SELF = this; + + if ( options.data ) { + SELF.state.data = _.clone( options.data ); + + delete SELF.state.data.page; + } else { + SELF.state.data = options.data = {}; + } + + if ( typeof options.data.page === 'undefined' ) { + SELF.state.currentPage = null; + SELF.state.totalPages = null; + SELF.state.totalObjects = null; + } else { + SELF.state.currentPage = options.data.page - 1; + } + + var success = options.success; + options.success = function( data, textStatus, request ) { + SELF.state.totalPages = parseInt( request.getResponseHeader( 'X-WP-TotalPages' ), 10 ); + SELF.state.totalObjects = parseInt( request.getResponseHeader( 'X-WP-Total' ), 10 ); + + if ( SELF.state.currentPage === null ) { + SELF.state.currentPage = 1; + } else { + SELF.state.currentPage++; + } + + if ( success ) { + return success.apply( this, arguments ); + } + }; + } + + return Backbone.sync( method, model, options ); + }, + + /** + * Fetches the next page of objects if a new page exists + * + * @param {data: {page}} options + * @returns {*} + */ + more: function( options ) { + options = options || {}; + options.data = options.data || {}; + + _.extend( options.data, this.state.data ); + + if ( typeof options.data.page === 'undefined' ) { + if ( ! this.hasMore() ) { + return false; + } + + if ( this.state.currentPage === null || this.state.currentPage <= 1 ) { + options.data.page = 2; + } else { + options.data.page = this.state.currentPage + 1; + } + } + + return this.fetch( options ); + }, + + /** + * Returns true if there are more pages of objects available + * + * @returns null|boolean + */ + hasMore: function() { + if ( this.state.totalPages === null || + this.state.totalObjects === null || + this.state.currentPage === null ) { + return null; + } else { + return ( this.state.currentPage < this.state.totalPages ); + } + } + } + ); + + /** + * Backbone collection for posts + */ + wp.api.collections.Posts = BaseCollection.extend( + /** @lends Posts.prototype */ + { + url: WP_API_Settings.root + '/posts', + + model: wp.api.models.Post + } + ); + + /** + * Backbone collection for pages + */ + wp.api.collections.Pages = BaseCollection.extend( + /** @lends Pages.prototype */ + { + url: WP_API_Settings.root + '/pages', + + model: wp.api.models.Page + } + ); + + /** + * Backbone users collection + */ + wp.api.collections.Users = BaseCollection.extend( + /** @lends Users.prototype */ + { + url: WP_API_Settings.root + '/users', + + model: wp.api.models.User + } + ); + + /** + * Backbone post statuses collection + */ + wp.api.collections.PostStatuses = BaseCollection.extend( + /** @lends PostStatuses.prototype */ + { + url: WP_API_Settings.root + '/posts/statuses', + + model: wp.api.models.PostStatus + + } + ); + + /** + * Backbone media library collection + */ + wp.api.collections.MediaLibrary = BaseCollection.extend( + /** @lends MediaLibrary.prototype */ + { + url: WP_API_Settings.root + '/media', + + model: wp.api.models.Media + } + ); + + /** + * Backbone taxonomy collection + */ + wp.api.collections.Taxonomies = BaseCollection.extend( + /** @lends Taxonomies.prototype */ + { + model: wp.api.models.Taxonomy, + + url: WP_API_Settings.root + '/taxonomies' + } + ); + + /** + * Backbone comment collection + */ + wp.api.collections.Comments = BaseCollection.extend( + /** @lends Comments.prototype */ + { + model: wp.api.models.Comment, + + post: null, + + /** + * @class Represent an array of comments + * @augments Backbone.Collection + * @constructs + */ + initialize: function( models, options ) { + this.constructor.__super__.initialize.apply( this, arguments ); + + if ( options && options.post ) { + this.post = options.post; + } + }, + + /** + * Return URL for collection + * + * @returns {string} + */ + url: function() { + return WP_API_Settings.root + '/posts/' + this.post + '/comments'; + } + } + ); + + /** + * Backbone post type collection + */ + wp.api.collections.PostTypes = BaseCollection.extend( + /** @lends PostTypes.prototype */ + { + model: wp.api.models.PostType, + + url: WP_API_Settings.root + '/posts/types' + } + ); + + /** + * Backbone terms collection + */ + wp.api.collections.Terms = BaseCollection.extend( + /** @lends Terms.prototype */ + { + model: wp.api.models.Term, + + type: 'post', + + taxonomy: 'category', + + /** + * @class Represent an array of terms + * @augments Backbone.Collection + * @constructs + */ + initialize: function( models, options ) { + this.constructor.__super__.initialize.apply( this, arguments ); + + if ( typeof options !== 'undefined' ) { + if ( options.type ) { + this.type = options.type; + } + + if ( options.taxonomy ) { + this.taxonomy = options.taxonomy; + } + } + + this.on( 'add', _.bind( this.addModel, this ) ); + }, + + /** + * We need to set the type and taxonomy for each model + * + * @param {Backbone.model} model + */ + addModel: function( model ) { + model.type = this.type; + model.taxonomy = this.taxonomy; + }, + + /** + * Return URL for collection + * + * @returns {string} + */ + url: function() { + return WP_API_Settings.root + '/posts/types/' + this.type + '/taxonomies/' + this.taxonomy + '/terms/'; + } + } + ); + + /** + * Backbone revisions collection + */ + wp.api.collections.Revisions = BaseCollection.extend( + /** @lends Revisions.prototype */ + { + model: wp.api.models.Revision, + + parent: null, + + /** + * @class Represent an array of revisions + * @augments Backbone.Collection + * @constructs + */ + initialize: function( models, options ) { + this.constructor.__super__.initialize.apply( this, arguments ); + + if ( options && options.parent ) { + this.parent = options.parent; + } + }, + + /** + * return URL for collection + * + * @returns {string} + */ + url: function() { + return WP_API_Settings.root + '/posts/' + this.parent + '/revisions'; + } + } + ); + +})( wp, WP_API_Settings, Backbone, _, window ); From f2b3fd1a75f166d7de361d5e032b9f5d98fcc39d Mon Sep 17 00:00:00 2001 From: Jeroen van Dijk Date: Sun, 1 Feb 2015 16:50:55 +0100 Subject: [PATCH 483/515] Fix incorrect reference to array --- lib/class-wp-json-media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-media.php b/lib/class-wp-json-media.php index 96b7c545a2..38fbfc2a63 100644 --- a/lib/class-wp-json-media.php +++ b/lib/class-wp-json-media.php @@ -434,7 +434,7 @@ public function attach_thumbnail( $post, $data, $update ) { // Already verified in preinsert_check() $thumbnail = $this->get_post( $data['featured_image'], 'child' ); - set_post_thumbnail( $post['ID'], $thumbnail['ID'] ); + set_post_thumbnail( $post['ID'], $thumbnail->data['ID'] ); } } From 605e34258412d6d0aa648c7d6177c37b2066d333 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 27 Feb 2015 11:48:38 +1000 Subject: [PATCH 484/515] Revert "WP_JSON_Posts::get_post_types() should return array, not hash/object" --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 32927c5e39..4a705da2bd 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -399,7 +399,7 @@ public function get_post_types() { continue; } - $types[] = $type; + $types[ $name ] = $type; } return $types; From 7f35541f0e698d2daf628433db951276cb017314 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 13 Mar 2015 16:57:53 -0500 Subject: [PATCH 485/515] Change project name and update author uri and plugin uri. --- plugin.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin.php b/plugin.php index b56a8eacba..9ff934c9b4 100644 --- a/plugin.php +++ b/plugin.php @@ -1,11 +1,11 @@ Date: Fri, 13 Mar 2015 16:58:23 -0500 Subject: [PATCH 486/515] Bump version number. --- plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.php b/plugin.php index 9ff934c9b4..def2a76dd4 100644 --- a/plugin.php +++ b/plugin.php @@ -2,7 +2,7 @@ /** * Plugin Name: WP REST API * Description: JSON-based REST API for WordPress, developed as part of GSoC 2013. - * Version: 1.1.1 + * Version: 1.2.0 * Author: WP REST API Team * Author URI: http://wp-api.org/ * Plugin URI: https://github.com/WP-API/WP-API @@ -13,7 +13,7 @@ * * @var string */ -define( 'JSON_API_VERSION', '1.1.1' ); +define( 'JSON_API_VERSION', '1.2.0' ); /** * Include our files for the API. From 911da351fded62e775dc3ca66eb094d63614cd41 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sat, 14 Mar 2015 11:43:31 -0500 Subject: [PATCH 487/515] Add timezone to comment dates. --- lib/class-wp-json-comments.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/class-wp-json-comments.php b/lib/class-wp-json-comments.php index 75f8b0f2ee..cc65de3ac6 100644 --- a/lib/class-wp-json-comments.php +++ b/lib/class-wp-json-comments.php @@ -187,7 +187,11 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen } // Date + $timezone = json_get_timezone(); + $comment_date = WP_JSON_DateTime::createFromFormat( 'Y-m-d H:i:s', $comment->comment_date, $timezone ); + $fields['date'] = json_mysql_to_rfc3339( $comment->comment_date ); + $fields['date_tz'] = $comment_date->format( 'e' ); $fields['date_gmt'] = json_mysql_to_rfc3339( $comment->comment_date_gmt ); // Meta From 1974318c1b0700d379ecc5353ee64f712dbec237 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sat, 14 Mar 2015 11:49:50 -0500 Subject: [PATCH 488/515] Add scrutinizer to master --- .scrutinizer.yml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .scrutinizer.yml diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000000..d8019385db --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,40 @@ +# Scrutinizer Configuration File + +tools: + php_sim: + enabled: true + min_mass: 50 + + php_pdepend: + enabled: true + configuration_file: null + suffixes: + - php + excluded_dirs: { } + + php_analyzer: + enabled: true + extensions: + - php + dependency_paths: { } + path_configs: { } + + php_changetracking: + enabled: true + bug_patterns: + - '\bfix(?:es|ed)?\b' + feature_patterns: + - '\badd(?:s|ed)?\b' + - '\bimplement(?:s|ed)?\b' + + php_hhvm: + enabled: true + command: hhvm + extensions: + - php + + path_configs: { } +before_commands: { } +after_commands: { } +artifacts: { } +build_failure_conditions: { } \ No newline at end of file From 4a1a0c918a7dd3ab91ecf4260fc3d277fd747f82 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Fri, 20 Mar 2015 14:47:34 -0500 Subject: [PATCH 489/515] Initial pass at the v1.2 changelog. --- CHANGELOG.md | 320 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a320ee40b9..8c37c223a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,325 @@ # Changelog +## 1.2.0 + +- Add handling for Cross-Origin Resource Sharing (CORS) OPTIONS requests. + + Preflighted requests (using the OPTIONS method) include the headers + `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, and + `Access-Control-Allow-Credentials` in the response, if the HTTP origin is + set. + + (props @rmccue, [#281][gh-281]) + +- Allow overriding full requests. + + The `json_pre_dispatch` filter allows a request to be hijacked before it is + dispatched. Hijacked requests can be anything a normal endpoint can return. + + (props @rmccue, [#281][gh-281]) + +- Check for JSON encoding/decoding errors. + + Returns the last error (if any) occurred during the last JSON encoding or + decoding operation. + + (props @joshkadis, @rmccue, [#461][gh-461]) + +- Add filtering to the terms collection endpoint. + + Available filter arguments are based on the `get_terms()` function. Example: + `/taxonomies/category/terms?filter[number]=10` would limit the response to 10 + category terms. + + (props @mauteri, [#401][gh-401], [#347][gh-347]) + +- Add handling for the `role` parameter when creating or updating a user. + + Allow users to be created or updated with a provided `role`. + + (props @pippinsplugins, [#392][gh-392], [#335][gh-335]) + +- Add handling for the `post_id` parameter when creating media. + + Allow passing the `post_id` parameter to associate a new media item with + a post. + + (props @pkevan, [#294][gh-294]) + +- Handle route matching for `-` in taxonomy and terms. + + Previously the regular expression used to match taxonomy and term names did + not support names with dashes. + + (props @EdHurtig, @evansobkowicz, [#410][gh-410]) + +- Handle JSONP callback matching for `.` in the function name. + + Previously the regular expression used to match JSONP callback functions did + not support names with periods. + + (props @codonnell822, [#455][gh-455]) + +- Fix the Content-Type header for JSONP requests. + + Previously JSONP requests sent the incorrect `application/json` Content-Type + header with the response. This would result in an error if strict MIME + checking was enabled. The Content-Type header was corrected to + `application/javascript` for JSONP responses. + + (props @simonlampen, [#380][gh-380]) + +- Add `$context` parameter to `json_prepare_term` filter. + + Terms responses can now be modified based on the `context` parameter of the + request. + + (props @traversal, [#316][gh-316]) + +- Move the JavaScript client library into the plugin. + + Previously, the `wp-api.js` file was a separate repository. The JavaScript + client has moved back into the plugin to coordinate code changes. + + (props @tlovett1, [#730][gh-730]) + +- Always return an object for media sizes + + The media sizes value should always be an object even when empty. Previously, + if a media item did not have any sizes set, an empty array was returned. + + **Compatibility warning**: Clients should be prepared to accept an empty + object as a value for media sizes. + + (props @maxcutler, [#300][gh-300]) + +- Give top-level posts a `null` parent value. + + For date type consistency, post parent property should be `null`. Previously, + parent-less posts returned `0` for parent. + + **Compatibility warning**: Clients should be prepared to accept `null` as a + value for post parent. + + (props @maxcutler, [#391][gh-391]) + +- Move permission checks out of `WP_JSON_Posts`. + + Introduce `json_check_post_permission()` function to allow post object + capability checks to be used outside the `WP_JSON_Posts` class. + + **Deprecation warning:** Calling `WP_JSON_Posts::check_read_permission` and + `WP_JSON_Posts::check_edit_permission` is now deprecated. + + (props @rachelbaker, [#486][gh-486], [#378][gh-378]) + +- Split comment endpoints into separate class. + + All comment handling has moved to the `WP_JSON_Comments` class. + + **Deprecation warning:** Calling `WP_JSON_Posts::get_comments`, + `WP_JSON_Posts::get_comment`, `WP_JSON_Posts::delete_comment`, and + `WP_JSON_Posts::prepare_comment` is now deprecated. + + (props @whyisjake, @rmccue, @rachelbaker, [#378][gh-378]) + +- Split meta endpoints into separate class. + + All post meta handling has moved to the new `WP_JSON_Meta_Posts` class. + + **Deprecation warning:** Calling `WP_JSON_Posts::get_all_meta`, + `WP_JSON_Posts::get_meta`, `WP_JSON_Posts::update_meta`, + `WP_JSON_Posts::add_meta`, `WP_JSON_Posts::delete_meta`, + `WP_JSON_Posts::prepare_meta`, and `WP_JSON_Posts::is_valid_meta_data` is + now deprecated. + + (props @rmccue, @rachelbaker, [#358][gh-358], [#474][gh-474]) + +- Rename internal create methods. + + **Deprecation warning:** Calling `WP_JSON_Posts::new_post`, + `WP_JSON_CustomPostType::new_post` and `WP_JSON_Posts::new_post` + is now deprecated. + + (props @rachelbaker, @rmccue, [#374][gh-374], [#377][gh-377], [#376][gh-376]) + +- Fix discrepancies in edit and create posts documentation examples. + + Corrected the edit and create posts code examples in the Getting Started + section. The new post example was updated to include the required + `content_raw` parameter. The new and edit posts examples were updated to use + a correct date parameter. + + (props @rachelbaker, [#305][gh-305]) + +- Update the cookie authentication documentation examples. + + With 1.1 the localized JavaScript object for `wp-api.js` changed to + `WP_API_Settings`. This updates the Authentication section documentation + nonce example to use the updated object name. + + (props @rachelbaker, [#321][gh-321]) + +- Add flexibly and multisite support to unit tests. + + Tests can be run from any WordPress install, and are not limited to only as + a plugin installed within a WordPress.org develop checkout. Unit tests are + now run against a multisite installation. + + (props @danielbachhuber, [#397][gh-397]) + +- Add `taxonomy` slug to the term response. + + (props @kalenjohnson, [#481][gh-481]) + +- Fix error when getting child comment. + + Previously an error occurred when a requested comment had a parent. + + (props @EdHurtig, [#413][gh-413], [#411][gh-411]) + +- Parse query strings before returning a JSON decode error. + + (props @jtsternberg, [#499][gh-499]) + +- Typecast the user ID parameter to be an integer for the `/users/{ID}` route. + + (props @dimadin, [#333][gh-333]) + +- Confirm a given JSONP callback is a string. + + (props @ircrash, @rmccue, [#405][gh-405]) + +- Register the JavaScript client in the admin. + + (props @tlovett1, [#473][gh-473]) + +- Remove duplicate error checks on post ids. + + (props @danielbachhuber, [#271][gh-271]) + +- Update documentation link references to wp-api.org. + + (props @pollyplummer, [#320][gh-320]) + +- Update documentation to note routes needing authentication. + + (props @kellbot, [#402][gh-402], [#309][gh-309]) + +- Correct Post route documentation filter parameters. + + (props @modemlooper, @rachelbaker, @rmccue, [#357][gh-357], [#462][gh-462]) + +- Update taxonomy route documentation with correct paths. + + (props @davidbhayes, [#364][gh-364], [#355][gh-355]) + +- Remove references to legacy `$fields` parameter. + + (props @JDGrimes, [#326][gh-326]) + +- Alter readme installation steps to use wp-cli for plugin and permalink setup. + + (props @kadamwhite, [#390][gh-390]) + +- Add steps to readme for executing tests with `vagrant ssh -c`. + + (props @kadamwhite, [#416][gh-416]) + +- Update readme to include provision step for testing suite. + + (props @ironpaperweight, [#396][gh-396]) + +- Update readme Getting Started link. + + (props @NikV, [#519][gh-519]) + +- Update readme Chassis repository links. + + (props @Japh, [#505][gh-505]) + +- Clean-up of `docs` folder. + + (props @pollyplummer, [#441][gh-441]) + +- Documentation audit for plugin.php file. + + (props @DrewAPicture, [#293][gh-293]) + +- Rename tests to match class file naming. + + (props @danielbachhuber, @rmccue, [#359][gh-359]) + +- Add license.txt file with license terms. + + (props @rachelbaker, [#393][gh-393], [#384][gh-384]) + +- Fix test_root when using WordPress.org developer checkout. + + (props @markoheijnen, [#437][gh-437]) + +[View all changes](https://github.com/rmccue/WP-API/compare/1.1.1...1.2) + +[gh-316]: https://github.com/WP-API/WP-API/issues/316 +[gh-305]: https://github.com/WP-API/WP-API/issues/305 +[gh-321]: https://github.com/WP-API/WP-API/issues/321 +[gh-333]: https://github.com/WP-API/WP-API/issues/333 +[gh-333]: https://github.com/WP-API/WP-API/issues/333 +[gh-326]: https://github.com/WP-API/WP-API/issues/326 +[gh-293]: https://github.com/WP-API/WP-API/issues/293 +[gh-300]: https://github.com/WP-API/WP-API/issues/300 +[gh-294]: https://github.com/WP-API/WP-API/issues/294 +[gh-320]: https://github.com/WP-API/WP-API/issues/320 +[gh-364]: https://github.com/WP-API/WP-API/issues/364 +[gh-355]: https://github.com/WP-API/WP-API/issues/355 +[gh-359]: https://github.com/WP-API/WP-API/issues/359 +[gh-357]: https://github.com/WP-API/WP-API/issues/357 +[gh-374]: https://github.com/WP-API/WP-API/issues/374 +[gh-377]: https://github.com/WP-API/WP-API/issues/377 +[gh-376]: https://github.com/WP-API/WP-API/issues/376 +[gh-390]: https://github.com/WP-API/WP-API/issues/390 +[gh-391]: https://github.com/WP-API/WP-API/issues/391 +[gh-393]: https://github.com/WP-API/WP-API/issues/393 +[gh-384]: https://github.com/WP-API/WP-API/issues/384 +[gh-396]: https://github.com/WP-API/WP-API/issues/396 +[gh-405]: https://github.com/WP-API/WP-API/issues/405 +[gh-380]: https://github.com/WP-API/WP-API/issues/380 +[gh-410]: https://github.com/WP-API/WP-API/issues/410 +[gh-416]: https://github.com/WP-API/WP-API/issues/416 +[gh-397]: https://github.com/WP-API/WP-API/issues/397 +[gh-437]: https://github.com/WP-API/WP-API/issues/437 +[gh-438]: https://github.com/WP-API/WP-API/issues/438 +[gh-402]: https://github.com/WP-API/WP-API/issues/402 +[gh-309]: https://github.com/WP-API/WP-API/issues/309 +[gh-413]: https://github.com/WP-API/WP-API/issues/413 +[gh-411]: https://github.com/WP-API/WP-API/issues/411 +[gh-458]: https://github.com/WP-API/WP-API/issues/458 +[gh-281]: https://github.com/WP-API/WP-API/issues/281 +[gh-392]: https://github.com/WP-API/WP-API/issues/392 +[gh-335]: https://github.com/WP-API/WP-API/issues/335 +[gh-271]: https://github.com/WP-API/WP-API/issues/271 +[gh-462]: https://github.com/WP-API/WP-API/issues/462 +[gh-461]: https://github.com/WP-API/WP-API/issues/461 +[gh-441]: https://github.com/WP-API/WP-API/issues/441 +[gh-455]: https://github.com/WP-API/WP-API/issues/455 +[gh-486]: https://github.com/WP-API/WP-API/issues/486 +[gh-358]: https://github.com/WP-API/WP-API/issues/358 +[gh-473]: https://github.com/WP-API/WP-API/issues/473 +[gh-505]: https://github.com/WP-API/WP-API/issues/505 +[gh-499]: https://github.com/WP-API/WP-API/issues/499 +[gh-519]: https://github.com/WP-API/WP-API/issues/519 +[gh-401]: https://github.com/WP-API/WP-API/issues/401 +[gh-347]: https://github.com/WP-API/WP-API/issues/347 +[gh-378]: https://github.com/WP-API/WP-API/issues/378 +[gh-528]: https://github.com/WP-API/WP-API/issues/528 +[gh-595]: https://github.com/WP-API/WP-API/issues/595 +[gh-730]: https://github.com/WP-API/WP-API/issues/730 +[gh-481]: https://github.com/WP-API/WP-API/issues/481 +[gh-524]: https://github.com/WP-API/WP-API/issues/524 +[gh-933]: https://github.com/WP-API/WP-API/issues/933 +[gh-474]: https://github.com/WP-API/WP-API/issues/474 +[gh-985]: https://github.com/WP-API/WP-API/issues/985 + ## 1.1.1 - Mitigate Flash CSRF exploit From d9acdb71fee159a150410145133565ff8e14a79d Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sat, 21 Mar 2015 11:41:24 -0500 Subject: [PATCH 490/515] Correct spelling and indentation in 1.2 changelog. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c37c223a8..c2d1255d5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -160,7 +160,7 @@ (props @rachelbaker, [#321][gh-321]) -- Add flexibly and multisite support to unit tests. +- Add flexibility and multisite support to unit tests. Tests can be run from any WordPress install, and are not limited to only as a plugin installed within a WordPress.org develop checkout. Unit tests are @@ -196,7 +196,7 @@ - Remove duplicate error checks on post ids. - (props @danielbachhuber, [#271][gh-271]) + (props @danielbachhuber, [#271][gh-271]) - Update documentation link references to wp-api.org. From 7b8a9eced87343ac8a695b1941e2f2a97010e7d7 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Sat, 21 Mar 2015 11:41:52 -0500 Subject: [PATCH 491/515] Order issues list for 1.2 changelog. --- CHANGELOG.md | 64 ++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d1255d5a..4b10d46549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -260,64 +260,64 @@ [View all changes](https://github.com/rmccue/WP-API/compare/1.1.1...1.2) -[gh-316]: https://github.com/WP-API/WP-API/issues/316 +[gh-271]: https://github.com/WP-API/WP-API/issues/271 +[gh-281]: https://github.com/WP-API/WP-API/issues/281 +[gh-293]: https://github.com/WP-API/WP-API/issues/293 +[gh-294]: https://github.com/WP-API/WP-API/issues/294 +[gh-300]: https://github.com/WP-API/WP-API/issues/300 [gh-305]: https://github.com/WP-API/WP-API/issues/305 +[gh-309]: https://github.com/WP-API/WP-API/issues/309 +[gh-316]: https://github.com/WP-API/WP-API/issues/316 +[gh-320]: https://github.com/WP-API/WP-API/issues/320 [gh-321]: https://github.com/WP-API/WP-API/issues/321 +[gh-326]: https://github.com/WP-API/WP-API/issues/326 [gh-333]: https://github.com/WP-API/WP-API/issues/333 [gh-333]: https://github.com/WP-API/WP-API/issues/333 -[gh-326]: https://github.com/WP-API/WP-API/issues/326 -[gh-293]: https://github.com/WP-API/WP-API/issues/293 -[gh-300]: https://github.com/WP-API/WP-API/issues/300 -[gh-294]: https://github.com/WP-API/WP-API/issues/294 -[gh-320]: https://github.com/WP-API/WP-API/issues/320 -[gh-364]: https://github.com/WP-API/WP-API/issues/364 +[gh-335]: https://github.com/WP-API/WP-API/issues/335 +[gh-347]: https://github.com/WP-API/WP-API/issues/347 [gh-355]: https://github.com/WP-API/WP-API/issues/355 -[gh-359]: https://github.com/WP-API/WP-API/issues/359 [gh-357]: https://github.com/WP-API/WP-API/issues/357 +[gh-358]: https://github.com/WP-API/WP-API/issues/358 +[gh-359]: https://github.com/WP-API/WP-API/issues/359 +[gh-364]: https://github.com/WP-API/WP-API/issues/364 [gh-374]: https://github.com/WP-API/WP-API/issues/374 -[gh-377]: https://github.com/WP-API/WP-API/issues/377 [gh-376]: https://github.com/WP-API/WP-API/issues/376 +[gh-377]: https://github.com/WP-API/WP-API/issues/377 +[gh-378]: https://github.com/WP-API/WP-API/issues/378 +[gh-380]: https://github.com/WP-API/WP-API/issues/380 +[gh-384]: https://github.com/WP-API/WP-API/issues/384 [gh-390]: https://github.com/WP-API/WP-API/issues/390 [gh-391]: https://github.com/WP-API/WP-API/issues/391 +[gh-392]: https://github.com/WP-API/WP-API/issues/392 [gh-393]: https://github.com/WP-API/WP-API/issues/393 -[gh-384]: https://github.com/WP-API/WP-API/issues/384 [gh-396]: https://github.com/WP-API/WP-API/issues/396 +[gh-397]: https://github.com/WP-API/WP-API/issues/397 +[gh-401]: https://github.com/WP-API/WP-API/issues/401 +[gh-402]: https://github.com/WP-API/WP-API/issues/402 [gh-405]: https://github.com/WP-API/WP-API/issues/405 -[gh-380]: https://github.com/WP-API/WP-API/issues/380 [gh-410]: https://github.com/WP-API/WP-API/issues/410 +[gh-411]: https://github.com/WP-API/WP-API/issues/411 +[gh-413]: https://github.com/WP-API/WP-API/issues/413 [gh-416]: https://github.com/WP-API/WP-API/issues/416 -[gh-397]: https://github.com/WP-API/WP-API/issues/397 [gh-437]: https://github.com/WP-API/WP-API/issues/437 [gh-438]: https://github.com/WP-API/WP-API/issues/438 -[gh-402]: https://github.com/WP-API/WP-API/issues/402 -[gh-309]: https://github.com/WP-API/WP-API/issues/309 -[gh-413]: https://github.com/WP-API/WP-API/issues/413 -[gh-411]: https://github.com/WP-API/WP-API/issues/411 -[gh-458]: https://github.com/WP-API/WP-API/issues/458 -[gh-281]: https://github.com/WP-API/WP-API/issues/281 -[gh-392]: https://github.com/WP-API/WP-API/issues/392 -[gh-335]: https://github.com/WP-API/WP-API/issues/335 -[gh-271]: https://github.com/WP-API/WP-API/issues/271 -[gh-462]: https://github.com/WP-API/WP-API/issues/462 -[gh-461]: https://github.com/WP-API/WP-API/issues/461 [gh-441]: https://github.com/WP-API/WP-API/issues/441 [gh-455]: https://github.com/WP-API/WP-API/issues/455 -[gh-486]: https://github.com/WP-API/WP-API/issues/486 -[gh-358]: https://github.com/WP-API/WP-API/issues/358 +[gh-458]: https://github.com/WP-API/WP-API/issues/458 +[gh-461]: https://github.com/WP-API/WP-API/issues/461 +[gh-462]: https://github.com/WP-API/WP-API/issues/462 [gh-473]: https://github.com/WP-API/WP-API/issues/473 -[gh-505]: https://github.com/WP-API/WP-API/issues/505 +[gh-474]: https://github.com/WP-API/WP-API/issues/474 +[gh-481]: https://github.com/WP-API/WP-API/issues/481 +[gh-486]: https://github.com/WP-API/WP-API/issues/486 [gh-499]: https://github.com/WP-API/WP-API/issues/499 +[gh-505]: https://github.com/WP-API/WP-API/issues/505 [gh-519]: https://github.com/WP-API/WP-API/issues/519 -[gh-401]: https://github.com/WP-API/WP-API/issues/401 -[gh-347]: https://github.com/WP-API/WP-API/issues/347 -[gh-378]: https://github.com/WP-API/WP-API/issues/378 +[gh-524]: https://github.com/WP-API/WP-API/issues/524 [gh-528]: https://github.com/WP-API/WP-API/issues/528 [gh-595]: https://github.com/WP-API/WP-API/issues/595 [gh-730]: https://github.com/WP-API/WP-API/issues/730 -[gh-481]: https://github.com/WP-API/WP-API/issues/481 -[gh-524]: https://github.com/WP-API/WP-API/issues/524 [gh-933]: https://github.com/WP-API/WP-API/issues/933 -[gh-474]: https://github.com/WP-API/WP-API/issues/474 [gh-985]: https://github.com/WP-API/WP-API/issues/985 ## 1.1.1 From 1204ab4e21576ca05bf1f109f9e97ef9ce39f4ed Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 30 Mar 2015 15:20:34 -0500 Subject: [PATCH 492/515] Remove filters that duplicate adding and getting meta functionality. --- plugin.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugin.php b/plugin.php index def2a76dd4..18b2014328 100644 --- a/plugin.php +++ b/plugin.php @@ -105,8 +105,6 @@ function json_api_default_filters( $server ) { // Post meta. $wp_json_post_meta = new WP_JSON_Meta_Posts( $server ); add_filter( 'json_endpoints', array( $wp_json_post_meta, 'register_routes' ), 0 ); - add_filter( 'json_prepare_post', array( $wp_json_post_meta, 'add_post_meta_data' ), 10, 3 ); - add_filter( 'json_insert_post', array( $wp_json_post_meta, 'insert_post_meta' ), 10, 2 ); // Media. $wp_json_media = new WP_JSON_Media( $server ); From 84bb9678230bad29b8522a67bd841f11662573dc Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Mon, 6 Apr 2015 16:42:38 -0400 Subject: [PATCH 493/515] Allow empty titles to be set after initial title is set --- lib/class-wp-json-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 9c5b411680..92d99e8a79 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -822,7 +822,7 @@ protected function insert_post( array $data ) { } // Post title - if ( ! empty( $data['title'] ) ) { + if ( isset( $data['title'] ) ) { $post['post_title'] = $data['title']; } From fe9f5cd1c83b8272d87b33aba839c084ea199e73 Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Mon, 6 Apr 2015 17:09:37 -0400 Subject: [PATCH 494/515] Add test for edit setting an empty title --- tests/test-json-posts.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test-json-posts.php b/tests/test-json-posts.php index 544bd866b3..0c6e9a823f 100644 --- a/tests/test-json-posts.php +++ b/tests/test-json-posts.php @@ -647,6 +647,14 @@ function test_edit_post() { $this->check_get_post_response( $response, $edited_post ); } + function test_edit_post_set_empty_title() { + $data = $this->set_data( array( 'ID' => $this->post_id, 'title' => '' ) ) ; + $this->endpoint->edit_post( $this->post_id, $data ); + + // Check that we have an empty title + $this->assertEquals( '', get_the_title( $this->post_id ) ); + } + function test_edit_post_without_permission() { $data = $this->set_data( array( 'ID' => $this->post_id ) ) ; From 620c1c438aabecafd8fca2f18727e57d03bc6a3c Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Thu, 9 Apr 2015 06:46:46 -0700 Subject: [PATCH 495/515] Ignore DS_Store on Macs --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..e43b0f9889 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store From b8ae4116f35529c7770cc2cbfc9c73e60365deb1 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Thu, 9 Apr 2015 07:47:31 -0700 Subject: [PATCH 496/515] Check read capability for revision parent --- lib/class-wp-json-posts.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 92d99e8a79..4d672a8902 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -271,7 +271,13 @@ public function get_post( $id, $context = 'view' ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } - if ( ! json_check_post_permission( $post, 'read' ) ) { + if ( 'inherit' === $post['post_status'] && $post['post_parent'] > 0 ) { + $checked_post = get_post( $post['post_parent'], ARRAY_A ); + } else { + $checked_post = $post; + } + + if ( ! json_check_post_permission( $checked_post, 'read' ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); } From bc10cf945461c7036e84971df1bf34617358158f Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Thu, 9 Apr 2015 08:29:35 -0700 Subject: [PATCH 497/515] Revisions require edit cap for parent --- lib/class-wp-json-posts.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index 4d672a8902..fb16668f75 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -271,13 +271,17 @@ public function get_post( $id, $context = 'view' ) { return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); } + $checked_permission = 'read'; if ( 'inherit' === $post['post_status'] && $post['post_parent'] > 0 ) { $checked_post = get_post( $post['post_parent'], ARRAY_A ); + if ( 'revision' === $post['post_type'] ) { + $checked_permission = 'edit'; + } } else { $checked_post = $post; } - if ( ! json_check_post_permission( $checked_post, 'read' ) ) { + if ( ! json_check_post_permission( $checked_post, $checked_permission ) ) { return new WP_Error( 'json_user_cannot_read', __( 'Sorry, you cannot read this post.' ), array( 'status' => 401 ) ); } From dda80d1ac53d9071c675c14d2d8b9cef3a43977e Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Thu, 9 Apr 2015 10:34:39 -0700 Subject: [PATCH 498/515] Add tests for revision access --- tests/test-json-post-revisions.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test-json-post-revisions.php b/tests/test-json-post-revisions.php index 29465dea24..ed85b724e8 100644 --- a/tests/test-json-post-revisions.php +++ b/tests/test-json-post-revisions.php @@ -78,4 +78,18 @@ public function test_post_no_revisions() { } -} \ No newline at end of file + public function test_access_single_revision() { + + wp_set_current_user( 0 ); + $response = $this->endpoint->get_post( $this->revision_id ); + $this->assertErrorResponse( 'json_user_cannot_read', $response, 401 ); + + wp_set_current_user( $this->author ); + $response = $this->endpoint->get_post( $this->revision_id ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + $response = json_ensure_response( $response ); + $this->assertEquals( 200, $response->get_status() ); + + } + +} From 083ff89b084126e42d45b3f8a94562edff9a20c0 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Thu, 9 Apr 2015 11:31:55 -0700 Subject: [PATCH 499/515] Update changelog; bump version to v1.2.1 --- CHANGELOG.md | 8 ++++++++ plugin.php | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b10d46549..0ae65501e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.2.1 + +- Fix information disclosure security vulnerability. + + 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. + + Reported by @chredd on 2015-04-09. + ## 1.2.0 - Add handling for Cross-Origin Resource Sharing (CORS) OPTIONS requests. diff --git a/plugin.php b/plugin.php index def2a76dd4..47474b7167 100644 --- a/plugin.php +++ b/plugin.php @@ -2,7 +2,7 @@ /** * Plugin Name: WP REST API * Description: JSON-based REST API for WordPress, developed as part of GSoC 2013. - * Version: 1.2.0 + * Version: 1.2.1 * Author: WP REST API Team * Author URI: http://wp-api.org/ * Plugin URI: https://github.com/WP-API/WP-API @@ -13,7 +13,7 @@ * * @var string */ -define( 'JSON_API_VERSION', '1.2.0' ); +define( 'JSON_API_VERSION', '1.2.1' ); /** * Include our files for the API. From 1f85ba4bf409d1b531fdc19d7bdfbcb7c1717aeb Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Fri, 17 Apr 2015 09:43:26 -0700 Subject: [PATCH 500/515] Only use if it's not WP_Error, and get data from Response object --- lib/class-wp-json-media.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-media.php b/lib/class-wp-json-media.php index 38fbfc2a63..301029d8be 100644 --- a/lib/class-wp-json-media.php +++ b/lib/class-wp-json-media.php @@ -433,8 +433,10 @@ public function attach_thumbnail( $post, $data, $update ) { if ( ! empty( $data['featured_image'] ) ) { // Already verified in preinsert_check() $thumbnail = $this->get_post( $data['featured_image'], 'child' ); - - set_post_thumbnail( $post['ID'], $thumbnail->data['ID'] ); + if ( ! is_wp_error( $thumbnail ) ) { + $data = $thumbnail->get_data(); + set_post_thumbnail( $post['ID'], $data['ID'] ); + } } } From c5956725c1b5249b7121c95100df9bf73bc1189d Mon Sep 17 00:00:00 2001 From: Harshit Sanghvi Date: Wed, 6 May 2015 16:02:48 -0400 Subject: [PATCH 501/515] Fix #1187. wpdevel submodule needs to be updated to latest from wordpress git mirror for phpunit to pass all the test cases. --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 76a38c98d3..40e3cc2dda 100644 --- a/README.md +++ b/README.md @@ -105,13 +105,22 @@ For testing, you'll need a little bit more: git clone --recursive https://github.com/Chassis/Tester.git extensions/tester ``` -2. Run the provisioner: +2. Update the `wpdevel` submodule in Chassis to latest on master from [WordPress Git Mirror](https://make.wordpress.org/core/2014/01/15/git-mirrors-for-wordpress/): + + ```bash + # From your base directory, api-tester if following the steps from before + cd extensions/tester/wpdevel + git checkout master + git pull + ``` + +3. Run the provisioner: ``` vagrant provision ``` -3. Log in to the virtual machine and run the testing suite: +4. Log in to the virtual machine and run the testing suite: ```bash vagrant ssh From c86c90d60694bf54e8720426c822852dd92bd88e Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 18 May 2015 17:00:29 -0500 Subject: [PATCH 502/515] Add check to make sure authenticated users are allowed to update their user role - return WP_Error if the current user does not have the expected permissions for a role change. - confirm the provided user-role exists. --- lib/class-wp-json-users.php | 41 +++++++++++++++++++++++++++++++++++ tests/test-json-users.php | 43 ++++++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-json-users.php b/lib/class-wp-json-users.php index 448a34359f..97129c3fd3 100644 --- a/lib/class-wp-json-users.php +++ b/lib/class-wp-json-users.php @@ -309,6 +309,13 @@ protected function insert_user( $data ) { // Role if ( ! empty( $data['role'] ) ) { + if ( $update ) { + $check_permission = $this->check_role_update( $user->ID, $data['role'] ); + if ( is_wp_error( $check_permission ) ) { + return $check_permission; + } + } + $user->role = $data['role']; } @@ -332,6 +339,37 @@ protected function insert_user( $data ) { return $user_id; } + /** + * Determine if the current user is allowed to make the desired role change. + * + * @param integer $user_id + * @param string $role + * @return boolen|WP_Error + */ + protected function check_role_update( $user_id, $role ) { + global $wp_roles; + + if ( ! isset( $wp_roles->role_objects[ $role ] ) ) { + return new WP_Error( 'json_user_invalid_role', __( 'Role is invalid.' ), array( 'status' => 400 ) ); + } + + $potential_role = $wp_roles->role_objects[ $role ]; + + // Don't let anyone with 'edit_users' (admins) edit their own role to something without it. + // Multisite super admins can freely edit their blog roles -- they possess all caps. + if ( ( is_multisite() && current_user_can( 'manage_sites' ) ) || get_current_user_id() !== $user_id || $potential_role->has_cap( 'edit_users' ) ) { + // The new role must be editable by the logged-in user. + $editable_roles = get_editable_roles(); + if ( empty( $editable_roles[ $role ] ) ) { + return new WP_Error( 'json_user_invalid_role', __( 'You cannot give users that role.' ), array( 'status' => 403 ) ); + } + + return true; + } + + return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give users that role.' ), array( 'status' => 403 ) ); + } + /** * Edit a user. * @@ -354,6 +392,9 @@ public function edit_user( $id, $data, $_headers = array() ) { if ( ! current_user_can( 'edit_user', $id ) ) { return new WP_Error( 'json_user_cannot_edit', __( 'Sorry, you are not allowed to edit this user.' ), array( 'status' => 403 ) ); } + if ( ! empty( $data['role'] ) && ! current_user_can( 'edit_users' ) ) { + return new WP_Error( 'json_cannot_edit_roles', __( 'Sorry, you are not allowed to edit roles of users.' ), array( 'status' => 403 ) ); + } $user = get_userdata( $id ); if ( ! $user ) { diff --git a/tests/test-json-users.php b/tests/test-json-users.php index b05a9879d1..5a868dc9da 100644 --- a/tests/test-json-users.php +++ b/tests/test-json-users.php @@ -6,7 +6,7 @@ * @package WordPress * @subpackage JSON API */ -class WP_Test_JSON_User extends WP_UnitTestCase { +class WP_Test_JSON_User extends WP_Test_JSON_TestCase { public function setUp() { parent::setUp(); @@ -205,4 +205,45 @@ public function test_update_user() { // as per https://core.trac.wordpress.org/ticket/21429 $this->assertEquals( $pw_before, $user->user_pass ); } + + + public function test_update_user_role() { + $admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); + $user_id = $this->factory->user->create( array( 'role' => 'author' ) ); + + wp_set_current_user( $admin_id ); + $admin = wp_get_current_user( $admin_id ); + $this->allow_user_to_create_users( $admin ); + + $response = $this->endpoint->edit_user( $user_id, array( + 'role' => 'editor', + ) ); + $this->assertNotInstanceOf( 'WP_Error', $response ); + + if ( ! $response instanceof WP_JSON_ResponseInterface ) { + $response = new WP_JSON_Response( $response ); + } + + // Check that we succeeded + $this->assertEquals( 200, $response->get_status() ); + + $user = get_userdata( $user_id ); + + $this->assertArrayHasKey( 'editor', $user->caps ); + } + + public function test_update_user_role_privilage_escalation() { + + $response = $this->endpoint->edit_user( $this->user, array( + 'role' => 'administrator' + ) ); + + $response = json_ensure_response( $response ); + + $this->assertErrorResponse( 'json_cannot_edit_roles', $response, 403 ); + + $user = get_userdata( $this->user ); + + $this->assertArrayHasKey( 'subscriber', $user->caps ); + } } From df7de979bc77c4dce6016b6a9abc0dc72fc071d3 Mon Sep 17 00:00:00 2001 From: Rachel Baker Date: Mon, 18 May 2015 17:01:09 -0500 Subject: [PATCH 503/515] Add Changelog entry and bump version to 1.2.2 --- CHANGELOG.md | 9 +++++++++ plugin.php | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ae65501e7..20e11a4c9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.2.2 + +- Fix user access security vulnerability. + + Authenticated users were able to escalate their privileges bypassing the + expected capabilities check. + + Reported by @kacperszurek on 2015-05-16. + ## 1.2.1 - Fix information disclosure security vulnerability. diff --git a/plugin.php b/plugin.php index 47474b7167..a6deaf0b36 100644 --- a/plugin.php +++ b/plugin.php @@ -2,7 +2,7 @@ /** * Plugin Name: WP REST API * Description: JSON-based REST API for WordPress, developed as part of GSoC 2013. - * Version: 1.2.1 + * Version: 1.2.2 * Author: WP REST API Team * Author URI: http://wp-api.org/ * Plugin URI: https://github.com/WP-API/WP-API @@ -13,7 +13,7 @@ * * @var string */ -define( 'JSON_API_VERSION', '1.2.1' ); +define( 'JSON_API_VERSION', '1.2.2' ); /** * Include our files for the API. From 35a007051c8d6da2994fe87bc00a185b7977ddee Mon Sep 17 00:00:00 2001 From: Saulo Paiva Date: Wed, 8 Jul 2015 17:33:08 -0300 Subject: [PATCH 504/515] Adds cast to integer in checking object metadata When deleting or updating post meta, the verification of meta belonging to object was failing because of "string - integer" comparison. --- lib/class-wp-json-meta.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-json-meta.php b/lib/class-wp-json-meta.php index 80bb13ee72..8c4cdd0b2f 100644 --- a/lib/class-wp-json-meta.php +++ b/lib/class-wp-json-meta.php @@ -241,7 +241,7 @@ public function update_meta( $id, $mid, $data ) { return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); } - if ( absint( $current->$parent_column ) !== $id ) { + if ( absint( $current->$parent_column ) !== (int) $id ) { return new WP_Error( 'json_meta_' . $this->type . '_mismatch', __( 'Meta does not belong to this object' ), array( 'status' => 400 ) ); } @@ -388,7 +388,7 @@ public function delete_meta( $id, $mid ) { return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); } - if ( absint( $current->$parent_column ) !== $id ) { + if ( absint( $current->$parent_column ) !== (int) $id ) { return new WP_Error( 'json_meta_' . $this->type . '_mismatch', __( 'Meta does not belong to this object' ), array( 'status' => 400 ) ); } From 774fd73ed053de2db36020debe142d6d3c58abc2 Mon Sep 17 00:00:00 2001 From: Saulo Paiva Date: Wed, 22 Jul 2015 14:49:33 -0300 Subject: [PATCH 505/515] Remove unnecessary integer cast in update_meta() --- lib/class-wp-json-meta.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-meta.php b/lib/class-wp-json-meta.php index 8c4cdd0b2f..5a01aa066c 100644 --- a/lib/class-wp-json-meta.php +++ b/lib/class-wp-json-meta.php @@ -241,7 +241,7 @@ public function update_meta( $id, $mid, $data ) { return new WP_Error( 'json_meta_invalid_id', __( 'Invalid meta ID.' ), array( 'status' => 404 ) ); } - if ( absint( $current->$parent_column ) !== (int) $id ) { + if ( absint( $current->$parent_column ) !== $id ) { return new WP_Error( 'json_meta_' . $this->type . '_mismatch', __( 'Meta does not belong to this object' ), array( 'status' => 400 ) ); } From da7e99ebb930ca1a8c36a3c5dcdbb3b7e4cff40d Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 10 Aug 2015 20:09:17 +0200 Subject: [PATCH 506/515] JSONP callback validation should match any --- lib/class-wp-json-server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index a19c10fb29..37f0fc0b0b 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -203,7 +203,7 @@ public function serve_request( $path = null ) { } // Check for invalid characters (only alphanumeric allowed) - if ( ! is_string( $_GET['_jsonp'] ) || preg_match( '/\W\./', $_GET['_jsonp'] ) ) { + if ( ! is_string( $_GET['_jsonp'] ) || preg_match( '/[^\w\.]/', $_GET['_jsonp'] ) ) { echo $this->json_error( 'json_callback_invalid', __( 'The JSONP callback function is invalid.' ), 400 ); return false; } From 27ceebd5e48b9c73c545182c7b80e3483f341b53 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Fri, 23 Oct 2015 11:06:57 -0700 Subject: [PATCH 507/515] Prioritize WP-API v1.x rewrite rules over core REST API When sites using WP-API v1.x upgrade to WordPress 4.4, they'll expect the plugin to continue working. Originally https://core.trac.wordpress.org/ticket/34399 --- plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.php b/plugin.php index bc2590d829..bca06c99d5 100644 --- a/plugin.php +++ b/plugin.php @@ -48,14 +48,14 @@ function json_api_init() { global $wp; $wp->add_query_var( 'json_route' ); } -add_action( 'init', 'json_api_init' ); +add_action( 'init', 'json_api_init', 11 ); // Prioritized over core rewrites /** * Add rewrite rules. */ function json_api_register_rewrites() { add_rewrite_rule( '^' . json_get_url_prefix() . '/?$','index.php?json_route=/','top' ); - add_rewrite_rule( '^' . json_get_url_prefix() . '(.*)?','index.php?json_route=$matches[1]','top' ); + add_rewrite_rule( '^' . json_get_url_prefix() . '/(.*)?','index.php?json_route=/$matches[1]','top' ); } /** From d12e11d9c05874700eb1dc6d0399c46398bf650c Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Wed, 11 Nov 2015 06:28:17 -0800 Subject: [PATCH 508/515] Run tests against WP nightly; fix tests for latest --- .travis.yml | 1 + bin/install-wp-tests.sh | 80 +++++++++++++++++++++++++++++++---------- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9e3862a7f9..6ef1b0d48b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ php: env: - WP_VERSION=latest WP_MULTISITE=0 - WP_VERSION=latest WP_MULTISITE=1 + - WP_VERSION=nightly WP_MULTISITE=0 # Clones WordPress and configures our testing environment. before_script: diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh index 1e39064a59..5baa6cb56c 100755 --- a/bin/install-wp-tests.sh +++ b/bin/install-wp-tests.sh @@ -12,23 +12,58 @@ DB_HOST=${4-localhost} WP_VERSION=${5-latest} WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} -WP_CORE_DIR=/tmp/wordpress/ +WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then + WP_TESTS_TAG="tags/$WP_VERSION" +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi set -ex install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + mkdir -p $WP_CORE_DIR - if [ $WP_VERSION == 'latest' ]; then - local ARCHIVE_NAME='latest' + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p /tmp/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip + unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ + mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR else - local ARCHIVE_NAME="wordpress-$WP_VERSION" + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz + tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR fi - wget -nv -O /tmp/wordpress.tar.gz http://wordpress.org/${ARCHIVE_NAME}.tar.gz - tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR - - wget -nv -O $WP_CORE_DIR/wp-content/db.php https://raw.github.com/markoheijnen/wp-mysqli/master/db.php + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php } install_test_suite() { @@ -39,17 +74,24 @@ install_test_suite() { local ioption='-i' fi - # set up testing suite - mkdir -p $WP_TESTS_DIR + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + fi + cd $WP_TESTS_DIR - svn co --quiet http://develop.svn.wordpress.org/trunk/tests/phpunit/includes/ - - wget -nv -O wp-tests-config.php http://develop.svn.wordpress.org/trunk/wp-tests-config-sample.php - sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" wp-tests-config.php - sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" wp-tests-config.php - sed $ioption "s/yourusernamehere/$DB_USER/" wp-tests-config.php - sed $ioption "s/yourpasswordhere/$DB_PASS/" wp-tests-config.php - sed $ioption "s|localhost|${DB_HOST}|" wp-tests-config.php + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + } install_db() { @@ -60,7 +102,7 @@ install_db() { local EXTRA="" if ! [ -z $DB_HOSTNAME ] ; then - if [[ "$DB_SOCK_OR_PORT" =~ ^[0-9]+$ ]] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" elif ! [ -z $DB_SOCK_OR_PORT ] ; then EXTRA=" --socket=$DB_SOCK_OR_PORT" From 38d403bd54dcec279311fd1fb78085fd9532da5e Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Wed, 11 Nov 2015 16:07:39 -0800 Subject: [PATCH 509/515] Update test to reflect new changed priority --- tests/test-json-plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test-json-plugin.php b/tests/test-json-plugin.php index 3e4bb8fb5f..cc9c15a3b7 100644 --- a/tests/test-json-plugin.php +++ b/tests/test-json-plugin.php @@ -20,10 +20,10 @@ function test_plugin_activated() { /** * The json_api_init hook should have been registered with init, and should - * have a default priority of 10. + * have a default priority of 11. */ function test_init_action_added() { - $this->assertEquals( 10, has_action( 'init', 'json_api_init' ) ); + $this->assertEquals( 11, has_action( 'init', 'json_api_init' ) ); } /** From 558473201c2f48ce1fdcd2488a0dad7460081cc6 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Wed, 11 Nov 2015 16:22:44 -0800 Subject: [PATCH 510/515] Fix failing test See e689747d88c512833f737daeb2757c57d5011b30 --- tests/test-json-plugin.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test-json-plugin.php b/tests/test-json-plugin.php index cc9c15a3b7..c5be32587c 100644 --- a/tests/test-json-plugin.php +++ b/tests/test-json-plugin.php @@ -30,6 +30,7 @@ function test_init_action_added() { * The json_route query variable should be registered. */ function test_json_route_query_var() { + json_api_init(); global $wp; $this->assertTrue( in_array( 'json_route', $wp->public_query_vars ) ); } From 71c40db94a65abdb392f22937b6d2d20a47a34d7 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Wed, 11 Nov 2015 16:31:12 -0800 Subject: [PATCH 511/515] Sync 1.2.3 from WordPress.org --- CHANGELOG.md | 8 ++++++++ lib/class-wp-json-server.php | 2 +- plugin.php | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e11a4c9a..acce4bf204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.2.3 + +- Fix potential XSS vulnerability. + + Requests from other origins could potentially run code on the API domain, allowing cross-origin access to authentication cookies or similar. + + Reported by @xknown on 2015-07-23. + ## 1.2.2 - Fix user access security vulnerability. diff --git a/lib/class-wp-json-server.php b/lib/class-wp-json-server.php index 37f0fc0b0b..359a39a0af 100644 --- a/lib/class-wp-json-server.php +++ b/lib/class-wp-json-server.php @@ -203,7 +203,7 @@ public function serve_request( $path = null ) { } // Check for invalid characters (only alphanumeric allowed) - if ( ! is_string( $_GET['_jsonp'] ) || preg_match( '/[^\w\.]/', $_GET['_jsonp'] ) ) { + if ( ! is_string( $_GET['_jsonp'] ) || preg_match( '/[^a-zA-Z0-9._]/', $_GET['_jsonp'] ) ) { echo $this->json_error( 'json_callback_invalid', __( 'The JSONP callback function is invalid.' ), 400 ); return false; } diff --git a/plugin.php b/plugin.php index bc2590d829..83c3b9ce0c 100644 --- a/plugin.php +++ b/plugin.php @@ -2,7 +2,7 @@ /** * Plugin Name: WP REST API * Description: JSON-based REST API for WordPress, developed as part of GSoC 2013. - * Version: 1.2.2 + * Version: 1.2.3 * Author: WP REST API Team * Author URI: http://wp-api.org/ * Plugin URI: https://github.com/WP-API/WP-API @@ -13,7 +13,7 @@ * * @var string */ -define( 'JSON_API_VERSION', '1.2.2' ); +define( 'JSON_API_VERSION', '1.2.3' ); /** * Include our files for the API. From 09fb2c17a074081fc5d750d1a0aad0b40016eff4 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Wed, 11 Nov 2015 16:42:59 -0800 Subject: [PATCH 512/515] Update stable to v1.2.4 --- CHANGELOG.md | 8 ++++++++ plugin.php | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acce4bf204..92ba0bc1c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.2.4 + +- Compatibilty with WordPress 4.4 + + Because WordPress 4.4 also registers rewrite rules for /wp-json/, WP-API v1 needs to register its rewrite rules with higher priority to continue to function as expected. + + (props @danielbachhuber) + ## 1.2.3 - Fix potential XSS vulnerability. diff --git a/plugin.php b/plugin.php index b0f663fe9f..a1cebcc19b 100644 --- a/plugin.php +++ b/plugin.php @@ -2,7 +2,7 @@ /** * Plugin Name: WP REST API * Description: JSON-based REST API for WordPress, developed as part of GSoC 2013. - * Version: 1.2.3 + * Version: 1.2.4 * Author: WP REST API Team * Author URI: http://wp-api.org/ * Plugin URI: https://github.com/WP-API/WP-API @@ -13,7 +13,7 @@ * * @var string */ -define( 'JSON_API_VERSION', '1.2.3' ); +define( 'JSON_API_VERSION', '1.2.4' ); /** * Include our files for the API. From e89f51b9815af1ef4a4bf39c421cba1d5dd6b2cb Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Mon, 11 Jan 2016 06:30:14 -0800 Subject: [PATCH 513/515] Ensure media of private posts are private too --- plugin.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugin.php b/plugin.php index a1cebcc19b..1ce7ebb518 100644 --- a/plugin.php +++ b/plugin.php @@ -521,10 +521,7 @@ function json_check_post_permission( $post, $capability = 'read' ) { // Can we read the parent if we're inheriting? if ( 'inherit' === $post['post_status'] && $post['post_parent'] > 0 ) { $parent = get_post( $post['post_parent'], ARRAY_A ); - - if ( json_check_post_permission( $parent, 'read' ) ) { - $permission = true; - } + return json_check_post_permission( $parent, 'read' ); } // If we don't have a parent, but the status is set to inherit, assume From 42ea5afbaee2caafbeaa9cf9ed59aa756b432e88 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Mon, 11 Jan 2016 07:40:11 -0800 Subject: [PATCH 514/515] Update changelog for 1.2.5 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92ba0bc1c0..58192c810f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.2.5 + +- Ensure media of private posts are private too. + + Reported by @danielbachhuber on 2016-01-08. + ## 1.2.4 - Compatibilty with WordPress 4.4 From bcffb19edabd4358d86c569e0e8e317db4489614 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Mon, 11 Jan 2016 08:54:48 -0800 Subject: [PATCH 515/515] Bump version to 1.2.5 --- plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.php b/plugin.php index 1ce7ebb518..916f78c6c6 100644 --- a/plugin.php +++ b/plugin.php @@ -2,7 +2,7 @@ /** * Plugin Name: WP REST API * Description: JSON-based REST API for WordPress, developed as part of GSoC 2013. - * Version: 1.2.4 + * Version: 1.2.5 * Author: WP REST API Team * Author URI: http://wp-api.org/ * Plugin URI: https://github.com/WP-API/WP-API @@ -13,7 +13,7 @@ * * @var string */ -define( 'JSON_API_VERSION', '1.2.4' ); +define( 'JSON_API_VERSION', '1.2.5' ); /** * Include our files for the API.