diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..05aff999e5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +# Travis CI Configuration File + +# Tell Travis CI we're using PHP +language: php + +php: + - "5.2" + - "5.3" + - "5.4" + - "5.5" + +# 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" + +script: phpunit 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/README.md b/README.md index 9c105cc24c..9a2fdc2c5c 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,18 @@ 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. +[![Build Status](https://travis-ci.org/WP-API/WP-API.png?branch=master)](https://travis-ci.org/WP-API/WP-API) + ## Documentation + Read the [plugin's documentation][docs]. -[docs]: https://github.com/rmccue/WP-API/tree/master/docs +[docs]: https://github.com/WP-API/WP-API/tree/master/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. @@ -24,8 +28,77 @@ working `PATH_INFO` on your server, but you don't need pretty permalinks enabled. +## Quick Setup + +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): + + ```bash + git clone --recursive git@github.com:sennza/Chassis.git api-tester + vagrant plugin install vagrant-hostsupdater + ``` + +3. Grab a copy of WP API: + + ```bash + cd api-tester + 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 + ``` + +4. Start the virtual machine: + + ```bash + vagrant up + ``` + +5. Browse to http://vagrant.local/wp/wp-admin/ and activate the WP API plugin + + ``` + Username: admin + Password: password + ``` + +6. Browse to http://vagrant.local/wp-json/ + + +### Testing + +For testing, you'll need a little bit more: + +1. SSH into your Vagrant box, and install PHPUnit: + + ```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 + ``` + +3. Run the testing suite: + + ```bash + cd /vagrant/content/plugins/json-rest-api + phpunit + ``` + + ## Issue Tracking -All tickets for the project are being tracked on the [GSoC Trac][]. Make sure -you use the JSON REST API component. -[GSoC Trac]: https://gsoc.trac.wordpress.org/query?component=JSON+REST+API +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. + +[GitHub]: https://github.com/WP-API/WP-API +[GSOC Trac]: https://gsoc.trac.wordpress.org/query?component=JSON+REST+API diff --git a/docs/guides/extending.md b/docs/guides/extending.md index 347f533a67..fad216325e 100644 --- a/docs/guides/extending.md +++ b/docs/guides/extending.md @@ -130,9 +130,9 @@ 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::getMyTypeItems()` might seem like a good name, it makes it +`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::getPosts()` with similar arguments to the parent is a +`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 @@ -159,7 +159,7 @@ 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 getMyData( $context, $_headers, $type = 'my-default-value' ) { + function get_my_data( $context, $_headers, $type = 'my-default-value' ) { if ( isset( $_headers['X-WP-EXAMPLE'] ) ) { $my_header_value = $_headers['X-WP-EXAMPLE']; } @@ -233,20 +233,20 @@ built-in types, your registration code should look something like this: global $myplugin_api_mytype; $myplugin_api_mytype = new MyPlugin_API_MyType(); - add_filter( 'json_endpoints', array( $myplugin_api_mytype, 'registerRoutes' ) ); + 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 registerRoutes( $routes ) { + public function register_routes( $routes ) { $routes['/myplugin/mytypeitems'] = array( - array( array( $this, 'getPosts'), WP_JSON_Server::READABLE ), - array( array( $this, 'newPost'), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), + 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, 'getPost'), WP_JSON_Server::READABLE ), - array( array( $this, 'editPost'), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), - array( array( $this, 'deletePost'), 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 ), ); // Add more custom routes here @@ -257,15 +257,18 @@ 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. + Alternatively, use the custom post type base class, which will handle the hooking and more for you: // main.php - function myplugin_api_init() { + 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(); + $myplugin_api_mytype = new MyPlugin_API_MyType( $server ); + $myplugin->register_filters(); } add_action( 'wp_json_server_before_serve', 'myplugin_api_init' ); @@ -274,10 +277,10 @@ hooking and more for you: protected $base = '/myplugin/mytypeitems'; protected $type = 'myplugin-mytype'; - public function registerRoutes( $routes ) { - $routes = parent::registerRoutes( $routes ); - // $routes = parent::registerRevisionRoutes( $routes ); - // $routes = parent::registerCommentRoutes( $routes ); + 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 diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index e771d8c299..8eb83e070f 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -25,15 +25,15 @@ 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. -The index is always available at the site's address with `/wp-json.php/` on the +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.php` followed by the route (which here is +start with `http://example.com/wp-json` followed by the route (which here is just `/`). Let's fire off the request: - curl -i http://example.com/wp-json.php/ + 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.) @@ -54,7 +54,7 @@ And here's what we get back: "GET" ], "meta": { - "self": "http:\/\/example.com\/wp-json.php\/" + "self": "http:\/\/example.com\/wp-json\/" } }, "\/posts": { @@ -64,7 +64,7 @@ And here's what we get back: "POST" ], "meta": { - "self": "http:\/\/example.com\/wp-json.php\/posts" + "self": "http:\/\/example.com\/wp-json\/posts" }, "accepts_json": true }, @@ -110,7 +110,7 @@ And here's what we get back: "GET" ], "meta": { - "self": "http:\/\/example.com\/wp-json.php\/posts\/types" + "self": "http:\/\/example.com\/wp-json\/posts\/types" } }, "\/posts\/types\/": { @@ -125,7 +125,7 @@ And here's what we get back: "GET" ], "meta": { - "self": "http:\/\/example.com\/wp-json.php\/posts\/statuses" + "self": "http:\/\/example.com\/wp-json\/posts\/statuses" } }, "\/taxonomies": { @@ -134,7 +134,7 @@ And here's what we get back: "GET" ], "meta": { - "self": "http:\/\/example.com\/wp-json.php\/taxonomies" + "self": "http:\/\/example.com\/wp-json\/taxonomies" } }, "\/taxonomies\/": { @@ -174,7 +174,7 @@ And here's what we get back: "POST" ], "meta": { - "self": "http:\/\/example.com\/wp-json.php\/users" + "self": "http:\/\/example.com\/wp-json\/users" }, "accepts_json": true }, @@ -188,7 +188,7 @@ And here's what we get back: "DELETE" ], "meta": { - "self": "http:\/\/example.com\/wp-json.php\/users\/me" + "self": "http:\/\/example.com\/wp-json\/users\/me" } }, "\/users\/": { @@ -273,13 +273,13 @@ 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.php/posts + 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!" + Link: ; rel="item"; title="Hello world!" [ { @@ -295,8 +295,8 @@ And this time, we get (again trimming headers, you'll have more than this): "avatar": "http:\/\/0.gravatar.com\/avatar\/c57c8945079831fa3c19caef02e44614&d=404&r=G", "meta": { "links": { - "self": "http:\/\/example.com\/wp-json.php\/users\/1", - "archives": "http:\/\/example.com\/wp-json.php\/users\/1\/posts" + "self": "http:\/\/example.com\/wp-json\/users\/1", + "archives": "http:\/\/example.com\/wp-json\/users\/1\/posts" } } }, @@ -316,19 +316,19 @@ And this time, we get (again trimming headers, you'll have more than this): "count": 1, "meta": { "links": { - "collection": "http:\/\/example.com\/wp-json.php\/taxonomy\/category", - "self": "http:\/\/example.com\/wp-json.php\/taxonomy\/category\/terms\/1" + "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.php\/posts\/1", - "author": "http:\/\/example.com\/wp-json.php\/users\/1", - "collection": "http:\/\/example.com\/wp-json.php\/posts", - "replies": "http:\/\/example.com\/wp-json.php\/posts\/1\/comments", - "version-history": "http:\/\/example.com\/wp-json.php\/posts\/1\/revisions" + "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" } } } @@ -353,14 +353,14 @@ Example of pagination headers: X-WP-Total: 492 X-WP-TotalPages: 50 - Link: ; rel="next", - ; rel="prev" + 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.php/posts/1 + curl -i http://example.com/wp-json/posts/1 Editing and Creating Posts @@ -381,7 +381,7 @@ the correct headers and authentication. The API uses HTTP Basic authentication: curl --data-binary="@updated-post.json" \ -H "Content-Type: application/javascript" \ --user admin:password \ - http://example.com/wp-json.php/posts/1 + 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. @@ -396,18 +396,18 @@ before, but this time, we POST it to the main posts route. curl --data-binary="@updated-post.json" \ -H "Content-Type: application/javascript" \ --user admin:password \ - http://example.com/wp-json.php/posts + 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.php/posts/2 + 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.php/posts/2 + curl -X DELETE --user admin:password http://example.com/wp-json/posts/2 In general, routes follow the same pattern: diff --git a/docs/guides/working-with-posts.md b/docs/guides/working-with-posts.md index cfc5a3e859..0d16603469 100644 --- a/docs/guides/working-with-posts.md +++ b/docs/guides/working-with-posts.md @@ -15,12 +15,12 @@ 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.php` in the main WP +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.php` and the example request is +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.php/posts HTTP/1.1 + GET /wp-json/posts HTTP/1.1 Host: example.com Higher level HTTP clients can usually handle this for you. @@ -60,7 +60,8 @@ The last parameter is the `filter` parameter. This gives you full access to 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. +searches) will be available. You can specify filter parameters in a request +using [array-style URL formatting][]. Creating and Editing Posts @@ -136,8 +137,8 @@ This should return a list of the available types: "hierarchical": false, "meta": { "links": { - "self": "http:\/\/example.com\/wp-json.php\/posts\/types\/post", - "archives": "http:\/\/example.com\/wp-json.php\/posts" + "self": "http:\/\/example.com\/wp-json\/posts\/types\/post", + "archives": "http:\/\/example.com\/wp-json\/posts" } } }, @@ -166,7 +167,7 @@ This should return a list of the available types: "hierarchical": true, "meta": { "links": { - "self": "http:\/\/example.com\/wp-json.php\/posts\/types\/page" + "self": "http:\/\/example.com\/wp-json\/posts\/types\/page" } } }, @@ -195,8 +196,8 @@ This should return a list of the available types: "hierarchical": false, "meta": { "links": { - "self": "http:\/\/example.com\/wp-json.php\/posts\/types\/attachment", - "archives": "http:\/\/example.com\/wp-json.php\/posts?type=attachment" + "self": "http:\/\/example.com\/wp-json\/posts\/types\/attachment", + "archives": "http:\/\/example.com\/wp-json\/posts?type=attachment" } } } @@ -220,7 +221,7 @@ A similar API exists for post statuses at `/posts/statuses`: "queryable": true, "show_in_list": true, "meta": { - "archives": "http:\/\/example.com\/wp-json.php\/posts" + "archives": "http:\/\/example.com\/wp-json\/posts" } }, "future": { @@ -283,3 +284,4 @@ take a look at the other APIs, or look at documentation on the specifics. [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 diff --git a/docs/schema.md b/docs/schema.md index bd486b9df0..1b0d922074 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -102,7 +102,7 @@ value indicating a human-readable documentation page about the API. "GET" ], "meta": { - "self": "http:\/\/example.com\/wp-json.php\/" + "self": "http:\/\/example.com\/wp-json\/" } }, "\/posts": { @@ -112,7 +112,7 @@ value indicating a human-readable documentation page about the API. "POST" ], "meta": { - "self": "http:\/\/example.com\/wp-json.php\/posts" + "self": "http:\/\/example.com\/wp-json\/posts" }, "accepts_json": true }, @@ -365,8 +365,8 @@ representation. "avatar": "http:\/\/0.gravatar.com\/avatar\/c57c8945079831fa3c19caef02e44614&d=404&r=G", "meta": { "links": { - "self": "http:\/\/example.com\/wp-json.php\/users\/1", - "archives": "http:\/\/example.com\/wp-json.php\/users\/1\/posts" + "self": "http:\/\/example.com\/wp-json\/users\/1", + "archives": "http:\/\/example.com\/wp-json\/users\/1\/posts" } }, "first_name": "", @@ -394,11 +394,11 @@ representation. ], "meta": { "links": { - "self": "http:\/\/example.com\/wp-json.php\/posts\/1", - "author": "http:\/\/example.com\/wp-json.php\/users\/1", - "collection": "http:\/\/example.com\/wp-json.php\/posts", - "replies": "http:\/\/example.com\/wp-json.php\/posts\/1\/comments", - "version-history": "http:\/\/example.com\/wp-json.php\/posts\/1\/revisions" + "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, @@ -411,8 +411,8 @@ representation. "count": 7, "meta": { "links": { - "collection": "http:\/\/example.com\/wp-json.php\/posts\/types\/post\/taxonomies\/category\/terms", - "self": "http:\/\/example.com\/wp-json.php\/posts\/types\/post\/taxonomies\/category\/terms\/1" + "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" } } } @@ -724,10 +724,10 @@ The body of a Post document is a Post entity. 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" + Link: ; rel="author" + Link: ; rel="collection" + Link: ; rel="replies" + Link: ; rel="version-history" Content-Type: application/json; charset=UTF-8 { @@ -743,8 +743,8 @@ The body of a Post document is a Post entity. "avatar":"http:\/\/0.gravatar.com\/avatar\/c57c8945079831fa3c19caef02e44614&d=404&r=G", "meta":{ "links":{ - "self":"http:\/\/localhost\/wptrunk\/wp-json.php\/users\/1", - "archives":"http:\/\/localhost\/wptrunk\/wp-json.php\/users\/1\/posts" + "self":"http:\/\/localhost\/wptrunk\/wp-json\/users\/1", + "archives":"http:\/\/localhost\/wptrunk\/wp-json\/users\/1\/posts" } } }, @@ -776,8 +776,8 @@ The body of a Post document is a Post entity. "count":4, "meta":{ "links":{ - "collection":"http:\/\/localhost\/wptrunk\/wp-json.php\/taxonomy\/category", - "self":"http:\/\/localhost\/wptrunk\/wp-json.php\/taxonomy\/category\/terms\/1" + "collection":"http:\/\/localhost\/wptrunk\/wp-json\/taxonomy\/category", + "self":"http:\/\/localhost\/wptrunk\/wp-json\/taxonomy\/category\/terms\/1" } } } @@ -785,11 +785,11 @@ The body of a Post document is a Post entity. "post_meta":[], "meta":{ "links":{ - "self":"http:\/\/localhost\/wptrunk\/wp-json.php\/posts\/158", - "author":"http:\/\/localhost\/wptrunk\/wp-json.php\/users\/1", - "collection":"http:\/\/localhost\/wptrunk\/wp-json.php\/posts", - "replies":"http:\/\/localhost\/wptrunk\/wp-json.php\/posts\/158\/comments", - "version-history":"http:\/\/localhost\/wptrunk\/wp-json.php\/posts\/158\/revisions" + "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" } } } diff --git a/lib/class-wp-json-customposttype.php b/lib/class-wp-json-customposttype.php index c72966ab48..b3c942cd51 100644 --- a/lib/class-wp-json-customposttype.php +++ b/lib/class-wp-json-customposttype.php @@ -38,28 +38,36 @@ public function __construct(WP_JSON_ResponseHandler $server) { return; } - add_filter( 'json_endpoints', array( $this, 'registerRoutes' ) ); - add_filter( 'json_post_type_data', array( $this, 'type_archive_link' ), 10, 2 ); - 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 ); + } + /** * Register the routes for the post type * * @param array $routes Routes for the post type * @return array Modified routes */ - public function registerRoutes( $routes ) { + public function register_routes( $routes ) { $routes[ $this->base ] = array( - array( array( $this, 'getPosts' ), WP_JSON_Server::READABLE ), - array( array( $this, 'newPost' ), WP_JSON_Server::CREATABLE ), + array( array( $this, 'get_posts' ), WP_JSON_Server::READABLE ), + array( array( $this, 'new_post' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), ); $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'getPost' ), WP_JSON_Server::READABLE ), - array( array( $this, 'editPost' ), WP_JSON_Server::EDITABLE ), - array( array( $this, 'deletePost' ), 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 ), ); return $routes; } @@ -70,7 +78,7 @@ public function registerRoutes( $routes ) { * @param array $routes Routes for the post type * @return array Modified routes */ - public function registerRevisionRoutes( $routes ) { + public function register_revision_routes( $routes ) { $routes[ $this->base . '/(?P\d+)/revisions' ] = array( array( '__return_null', WP_JSON_Server::READABLE ), ); @@ -83,13 +91,13 @@ public function registerRevisionRoutes( $routes ) { * @param array $routes Routes for the post type * @return array Modified routes */ - public function registerCommentRoutes( $routes ) { + public function register_comment_routes( $routes ) { $routes[ $this->base . '/(?P\d+)/comments'] = array( - array( array( $this, 'getComments' ), WP_JSON_Server::READABLE ), + 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, 'getComment' ), WP_JSON_Server::READABLE ), + 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 ), ); @@ -102,21 +110,21 @@ public function registerCommentRoutes( $routes ) { * Overrides the $type to set to $this->type, then passes through to the * post endpoints. * - * @see WP_JSON_Posts::getPosts() + * @see WP_JSON_Posts::get_posts() */ - public function getPosts( $filter = array(), $context = 'view', $type = null, $page = 1 ) { + public function get_posts( $filter = array(), $context = 'view', $type = null, $page = 1 ) { if ( !empty( $type ) && $type !== $this->type ) return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); - return parent::getPosts( $filter, $context, $this->type, $page ); + return parent::get_posts( $filter, $context, $this->type, $page ); } /** * Retrieve a post * - * @see WP_JSON_Posts::getPost() + * @see WP_JSON_Posts::get_post() */ - public function getPost( $id, $context = 'view' ) { + public function get_post( $id, $context = 'view' ) { $id = (int) $id; if ( empty( $id ) ) @@ -127,15 +135,15 @@ public function getPost( $id, $context = 'view' ) { if ( $post['post_type'] !== $this->type ) return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); - return parent::getPost( $id, $context ); + return parent::get_post( $id, $context ); } /** * Edit a post * - * @see WP_JSON_Posts::editPost() + * @see WP_JSON_Posts::edit_post() */ - function editPost( $id, $data, $_headers = array() ) { + 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 ) ); @@ -148,15 +156,15 @@ function editPost( $id, $data, $_headers = array() ) { if ( $post['post_type'] !== $this->type ) return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); - return parent::editPost( $id, $data, $_headers ); + return parent::edit_post( $id, $data, $_headers ); } /** * Delete a post * - * @see WP_JSON_Posts::deletePost() + * @see WP_JSON_Posts::delete_post() */ - public function deletePost( $id, $force = false ) { + public function delete_post( $id, $force = false ) { $id = (int) $id; if ( empty( $id ) ) @@ -167,7 +175,7 @@ public function deletePost( $id, $force = false ) { if ( $post['post_type'] !== $this->type ) return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); - return parent::deletePost( $id, $force ); + return parent::delete_post( $id, $force ); } /** diff --git a/lib/class-wp-json-datetime.php b/lib/class-wp-json-datetime.php new file mode 100644 index 0000000000..d2e93c15cc --- /dev/null +++ b/lib/class-wp-json-datetime.php @@ -0,0 +1,30 @@ + 5.2 + * + * @link http://stackoverflow.com/a/17084893/717643 + * + * @param string $format The format that the passed in string should be in. + * @param string $string String representing the time. + * @param DateTimeZone $timezone A DateTimeZone object representing the desired time zone. + * @return Datetime + */ + 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 diff --git a/lib/class-wp-json-media.php b/lib/class-wp-json-media.php index 9573d208a2..5ae475ee6c 100644 --- a/lib/class-wp-json-media.php +++ b/lib/class-wp-json-media.php @@ -7,17 +7,17 @@ class WP_JSON_Media extends WP_JSON_Posts { * @param array $routes Existing routes * @return array Modified routes */ - public function registerRoutes( $routes ) { + public function register_routes( $routes ) { $media_routes = array( '/media' => array( - array( array( $this, 'getPosts' ), WP_JSON_Server::READABLE ), - array( array( $this, 'uploadAttachment' ), WP_JSON_Server::CREATABLE ), + 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, 'getPost' ), WP_JSON_Server::READABLE ), - array( array( $this, 'editPost' ), WP_JSON_Server::EDITABLE ), - array( array( $this, 'deletePost' ), WP_JSON_Server::DELETABLE ), + array( array( $this, 'get_post' ), WP_JSON_Server::READABLE ), + array( array( $this, 'edit_post' ), WP_JSON_Server::EDITABLE ), + array( array( $this, 'delete_post' ), WP_JSON_Server::DELETABLE ), ), ); return array_merge( $routes, $media_routes ); @@ -29,9 +29,9 @@ public function registerRoutes( $routes ) { * Overrides the $type to set to 'attachment', then passes through to the post * endpoints. * - * @see WP_JSON_Posts::getPosts() + * @see WP_JSON_Posts::get_posts() */ - public function getPosts( $filter = array(), $context = 'view', $type = 'attachment', $page = 1 ) { + public function get_posts( $filter = array(), $context = 'view', $type = 'attachment', $page = 1 ) { if ( $type !== 'attachment' ) return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); @@ -39,10 +39,10 @@ public function getPosts( $filter = array(), $context = 'view', $type = 'attachm $filter['post_status'] = array( 'publish', 'inherit' ); // Always allow status queries for attachments - add_filter( 'query_vars', array( $this, 'allowStatusQuery' ) ); + add_filter( 'query_vars', array( $this, 'allow_status_query' ) ); } - $posts = parent::getPosts( $filter, $context, 'attachment', $page ); + $posts = parent::get_posts( $filter, $context, 'attachment', $page ); return $posts; } @@ -53,8 +53,8 @@ public function getPosts( $filter = array(), $context = 'view', $type = 'attachm * @param array $vars Query variables * @return array Filtered query variables */ - public function allowStatusQuery( $vars ) { - remove_filter( 'query_vars', array( $this, 'allowStatusQuery' ) ); + public function allow_status_query( $vars ) { + remove_filter( 'query_vars', array( $this, 'allow_status_query' ) ); $vars[] = 'post_status'; return $vars; @@ -63,9 +63,9 @@ public function allowStatusQuery( $vars ) { /** * Retrieve a attachment * - * @see WP_JSON_Posts::getPost() + * @see WP_JSON_Posts::get_post() */ - public function getPost( $id, $context = 'view' ) { + public function get_post( $id, $context = 'view' ) { $id = (int) $id; if ( empty( $id ) ) @@ -76,7 +76,7 @@ public function getPost( $id, $context = 'view' ) { if ( $post['post_type'] !== 'attachment' ) return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); - return parent::getPost( $id, $context ); + return parent::get_post( $id, $context ); } /** @@ -130,9 +130,9 @@ protected function prepare_post( $post, $context = 'single' ) { /** * Edit a attachment * - * @see WP_JSON_Posts::editPost() + * @see WP_JSON_Posts::edit_post() */ - public function editPost( $id, $data, $_headers = array() ) { + public 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 ) ); @@ -145,15 +145,15 @@ public function editPost( $id, $data, $_headers = array() ) { if ( $post['post_type'] !== 'attachment' ) return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); - return parent::editPost( $id, $data, $_headers ); + return parent::edit_post( $id, $data, $_headers ); } /** * Delete a attachment * - * @see WP_JSON_Posts::deletePost() + * @see WP_JSON_Posts::delete_post() */ - public function deletePost( $id, $force = false ) { + public function delete_post( $id, $force = false ) { $id = (int) $id; if ( empty( $id ) ) @@ -164,7 +164,7 @@ public function deletePost( $id, $force = false ) { if ( $post['post_type'] !== 'attachment' ) return new WP_Error( 'json_post_invalid_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); - return parent::deletePost( $id, $force ); + return parent::delete_post( $id, $force ); } /** @@ -178,7 +178,7 @@ public function deletePost( $id, $force = false ) { * @param array $_headers HTTP headers from the request * @return array|WP_Error Attachment data or error */ - public function uploadAttachment( $_files, $_headers ) { + public function upload_attachment( $_files, $_headers ) { $post_type = get_post_type_object( 'attachment' ); if ( ! $post_type ) return new WP_Error( 'json_invalid_post_type', __( 'Invalid post type' ), array( 'status' => 400 ) ); @@ -189,10 +189,10 @@ public function uploadAttachment( $_files, $_headers ) { // Get the file via $_FILES or raw data if ( empty( $_files ) ) { - $file = $this->uploadFromData( $_files, $_headers ); + $file = $this->upload_from_data( $_files, $_headers ); } else { - $file = $this->uploadFromFile( $_files, $_headers ); + $file = $this->upload_from_file( $_files, $_headers ); } if ( is_wp_error( $file ) ) @@ -236,9 +236,8 @@ public function uploadAttachment( $_files, $_headers ) { wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) ); } - $this->server->send_status( 201 ); - $this->server->header( 'Location', json_url( '/media/' . $id ) ); - return $this->getPost( $id, 'edit' ); + $headers = array( 'Location' => json_url( '/media/' . $id ) ); + return new WP_JSON_Response( $this->getPost( $id, 'edit' ), 201, $headers ); } /** @@ -248,7 +247,7 @@ public function uploadAttachment( $_files, $_headers ) { * @param array $_headers HTTP headers from the request * @return array|WP_Error Data from {@see wp_handle_sideload()} */ - protected function uploadFromData( $_files, $_headers ) { + protected function upload_from_data( $_files, $_headers ) { $data = $this->server->get_raw_data(); if ( empty( $data ) ) { @@ -329,7 +328,7 @@ protected function uploadFromData( $_files, $_headers ) { * @param array $_headers HTTP headers from the request * @return array|WP_Error Data from {@see wp_handle_upload()} */ - protected function uploadFromFile( $_files, $_headers ) { + protected function upload_from_file( $_files, $_headers ) { if ( empty( $_files['file'] ) ) return new WP_Error( 'json_upload_no_data', __( 'No data supplied' ), array( 'status' => 400 ) ); @@ -362,7 +361,7 @@ protected function uploadFromFile( $_files, $_headers ) { * @param array $data Supplied post data * @return bool|WP_Error Success or error object */ - public function preinsertCheck( $status, $post, $data ) { + public function preinsert_check( $status, $post, $data ) { if ( is_wp_error( $status ) ) { return $status; } @@ -385,11 +384,15 @@ public function preinsertCheck( $status, $post, $data ) { * @param array $data Supplied post data * @param boolean $update Is this an update? */ - public function attachThumbnail( $post, $data, $update ) { + public function attach_thumbnail( $post, $data, $update ) { + if ( ! $update ) { + return; + } + if ( ! empty( $data['featured_image'] ) ) { // Already verified in preinsertCheck() - $thumbnail = $this->getPost( $data['featured_image'], 'child' ); - set_post_thumbnail( $post_ID, $thumbnail['ID'] ); + $thumbnail = $this->get_post( $data['featured_image'], 'child' ); + set_post_thumbnail( $post['ID'], $thumbnail['ID'] ); } } @@ -401,7 +404,7 @@ public function attachThumbnail( $post, $data, $update ) { * @param string $context Display context * @return array Filtered post data */ - public function addThumbnailData( $data, $post, $context ) { + public function add_thumbnail_data( $data, $post, $context ) { if( !post_type_supports( $post['post_type'], 'thumbnail' ) ) { return $data; } @@ -410,7 +413,7 @@ public function addThumbnailData( $data, $post, $context ) { $data['featured_image'] = null; $thumbnail_id = get_post_thumbnail_id( $post['ID'] ); if ( $thumbnail_id ) { - $data['featured_image'] = $this->getPost( $thumbnail_id, 'child' ); + $data['featured_image'] = $this->get_post( $thumbnail_id, 'child' ); } return $data; diff --git a/lib/class-wp-json-pages.php b/lib/class-wp-json-pages.php index 5000c0925e..50d3ff2ab7 100644 --- a/lib/class-wp-json-pages.php +++ b/lib/class-wp-json-pages.php @@ -39,16 +39,16 @@ class WP_JSON_Pages extends WP_JSON_CustomPostType { * @param array $routes Existing routes * @return array Modified routes */ - public function registerRoutes( $routes ) { - $routes = parent::registerRoutes( $routes ); - $routes = parent::registerRevisionRoutes( $routes ); - $routes = parent::registerCommentRoutes( $routes ); + public function register_routes( $routes ) { + $routes = parent::register_routes( $routes ); + $routes = parent::register_revision_routes( $routes ); + $routes = parent::register_comment_routes( $routes ); // Add post-by-path routes $routes[ $this->base . '/(?P.+)'] = array( - array( array( $this, 'getPostByPath' ), WP_JSON_Server::READABLE ), - array( array( $this, 'editPostByPath' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), - array( array( $this, 'deletePostByPath' ), WP_JSON_Server::DELETABLE ), + array( array( $this, 'get_post_by_path' ), WP_JSON_Server::READABLE ), + array( array( $this, 'edit_post_by_path' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), + array( array( $this, 'delete_post_by_path' ), WP_JSON_Server::DELETABLE ), ); return $routes; @@ -59,13 +59,13 @@ public function registerRoutes( $routes ) { * * @param string $path */ - public function getPostByPath( $path, $context = 'view' ) { + public function get_post_by_path( $path, $context = 'view' ) { $post = get_page_by_path( $path, ARRAY_A ); if ( empty( $post ) ) return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - return $this->getPost( $post['ID'], $context ); + return $this->get_post( $post['ID'], $context ); } /** @@ -73,13 +73,13 @@ public function getPostByPath( $path, $context = 'view' ) { * * @param string $path */ - public function editPostByPath( $path, $data, $_headers = array() ) { + public function edit_post_by_path( $path, $data, $_headers = array() ) { $post = get_page_by_path( $path, ARRAY_A ); if ( empty( $post ) ) return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - return $this->editPost( $post['ID'], $data, $_headers ); + return $this->edit_post( $post['ID'], $data, $_headers ); } /** @@ -87,13 +87,13 @@ public function editPostByPath( $path, $data, $_headers = array() ) { * * @param string $path */ - public function deletePostByPath( $path, $force = false ) { + public function delete_post_by_path( $path, $force = false ) { $post = get_page_by_path( $path, ARRAY_A ); if ( empty( $post ) ) return new WP_Error( 'json_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) ); - return $this->deletePost( $post['ID'], $force ); + return $this->delete_post( $post['ID'], $force ); } /** @@ -107,10 +107,10 @@ protected function prepare_post( $post, $context = 'view' ) { $_post = parent::prepare_post( $post, $context ); // Override entity meta keys with the correct links - $_post['meta']['links']['self'] = json_url( '/pages/' . get_page_uri( $post['ID'] ) ); + $_post['meta']['links']['self'] = json_url( $this->base . '/' . get_page_uri( $post['ID'] ) ); if ( ! empty( $post['post_parent'] ) ) - $_post['meta']['links']['up'] = json_url( '/pages/' . get_page_uri( (int) $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 ); } diff --git a/lib/class-wp-json-posts.php b/lib/class-wp-json-posts.php index e440b25356..e371210ea6 100644 --- a/lib/class-wp-json-posts.php +++ b/lib/class-wp-json-posts.php @@ -23,36 +23,36 @@ 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 ) { $post_routes = array( // Post endpoints '/posts' => array( - array( array( $this, 'getPosts' ), WP_JSON_Server::READABLE ), - array( array( $this, 'newPost' ), WP_JSON_Server::CREATABLE | WP_JSON_Server::ACCEPT_JSON ), + 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, 'getPost' ), WP_JSON_Server::READABLE ), - array( array( $this, 'editPost' ), WP_JSON_Server::EDITABLE | WP_JSON_Server::ACCEPT_JSON ), - array( array( $this, 'deletePost' ), 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( '__return_null', WP_JSON_Server::READABLE ), // Comments '/posts/(?P\d+)/comments' => array( - array( array( $this, 'getComments' ), WP_JSON_Server::READABLE ), + 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, 'getComment' ), WP_JSON_Server::READABLE ), + 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 ), ), // Meta-post endpoints - '/posts/types' => array( array( $this, 'getPostTypes' ), WP_JSON_Server::READABLE ), - '/posts/types/(?P\w+)' => array( array( $this, 'getPostType' ), WP_JSON_Server::READABLE ), - '/posts/statuses' => array( array( $this, 'getPostStatuses' ), 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 ); } @@ -70,14 +70,14 @@ public function registerRoutes( $routes ) { * in the response array. * * @uses wp_get_recent_posts() - * @see WP_JSON_Posts::getPost() for more on $fields + * @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. */ - public function getPosts( $filter = array(), $context = 'view', $type = 'post', $page = 1 ) { + public function get_posts( $filter = array(), $context = 'view', $type = 'post', $page = 1 ) { $query = array(); $post_type = get_post_type_object( $type ); @@ -123,28 +123,32 @@ public function getPosts( $filter = array(), $context = 'view', $type = 'post', $post_query = new WP_Query(); $posts_list = $post_query->query( $query ); - $this->server->query_navigation_headers( $post_query ); + $response = new WP_JSON_Response(); + $response->query_navigation_headers( $post_query ); - if ( ! $posts_list ) - return array(); + if ( ! $posts_list ) { + $response->set_data( array() ); + return $response; + } // holds all the posts data $struct = array(); - $this->server->header( 'Last-Modified', mysql2date( 'D, d M Y H:i:s', get_lastpostmodified( 'GMT' ), 0 ).' GMT' ); + $response->header( 'Last-Modified', mysql2date( 'D, d M Y H:i:s', get_lastpostmodified( 'GMT' ), 0 ).' GMT' ); foreach ( $posts_list as $post ) { $post = get_object_vars( $post ); // Do we have permission to read this post? - if ( ! $this->checkReadPermission( $post ) ) + if ( ! $this->check_read_permission( $post ) ) continue; - $this->server->link_header( 'item', json_url( '/posts/' . $post['ID'] ), array( 'title' => $post['post_title'] ) ); + $response->link_header( 'item', json_url( '/posts/' . $post['ID'] ), array( 'title' => $post['post_title'] ) ); $struct[] = $this->prepare_post( $post, $context ); } + $response->set_data( $struct ); - return $struct; + return $response; } /** @@ -154,7 +158,7 @@ public function getPosts( $filter = array(), $context = 'view', $type = 'post', * @param array $post Post data * @return boolean Can we read it? */ - protected function checkReadPermission( $post ) { + protected function check_read_permission( $post ) { // Can we read the post? $post_type = get_post_type_object( $post['post_type'] ); if ( 'publish' === $post['post_status'] || current_user_can( $post_type->cap->read_post, $post['ID'] ) ) { @@ -165,7 +169,7 @@ protected function checkReadPermission( $post ) { if ( 'inherit' === $post['post_status'] && $post['post_parent'] > 0 ) { $parent = get_post( $post['post_parent'], ARRAY_A ); - if ( $this->checkReadPermission( $parent ) ) { + if ( $this->check_read_permission( $parent ) ) { return true; } } @@ -204,24 +208,20 @@ protected function checkReadPermission( $post ) { * - terms_names - array, with taxonomy names as keys and arrays of term names as values * - enclosure * - any other fields supported by wp_insert_post() - * @return array Post data (see {@see WP_JSON_Posts::getPost}) + * @return array Post data (see {@see WP_JSON_Posts::get_post}) */ - function newPost( $data ) { + function new_post( $data ) { unset( $data['ID'] ); $result = $this->insert_post( $data ); - if ( is_string( $result ) || is_int( $result ) ) { - $this->server->send_status( 201 ); - $this->server->header( 'Location', json_url( '/posts/' . $result ) ); - - return $this->getPost( $result ); - } - elseif ( $result instanceof IXR_Error ) { - return new WP_Error( 'json_insert_error', $result->message, array( 'status' => $result->code ) ); - } - else { - return new WP_Error( 'json_insert_error', __( 'An unknown error occurred while creating the post' ), array( 'status' => 500 ) ); + if ( $result instanceof WP_Error ) { + return $result; } + + $response = $this->get_post( $result ); + $response->set_status( 201 ); + $response->header( 'Location', json_url( '/posts/' . $result ) ); + return $response; } /** @@ -232,7 +232,7 @@ function newPost( $data ) { * @param array $fields Post fields to return (optional) * @return array Post entity */ - public function getPost( $id, $context = 'view' ) { + public function get_post( $id, $context = 'view' ) { $id = (int) $id; if ( empty( $id ) ) @@ -244,23 +244,25 @@ public function getPost( $id, $context = 'view' ) { 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->checkReadPermission( $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) - $this->server->header( 'Last-Modified', mysql2date( 'D, d M Y H:i:s', $post['post_modified_gmt'] ) . 'GMT' ); + $response = new WP_JSON_Response(); + $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 ) ) return $post; foreach ( $post['meta']['links'] as $rel => $url ) { - $this->server->link_header( $rel, $url ); + $response->link_header( $rel, $url ); } - $this->server->link_header( 'alternate', get_permalink( $id ), array( 'type' => 'text/html' ) ); + $response->link_header( 'alternate', get_permalink( $id ), array( 'type' => 'text/html' ) ); - return $post; + $response->set_data( $post ); + return $response; } /** @@ -273,11 +275,11 @@ public function getPost( $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::newPost} + * @param array $data Data construct, see {@see WP_JSON_Posts::new_post} * @param array $_headers Header data * @return true on success */ - function editPost( $id, $data, $_headers = array() ) { + function edit_post( $id, $data, $_headers = array() ) { $id = (int) $id; if ( empty( $id ) ) @@ -293,7 +295,7 @@ function editPost( $id, $data, $_headers = array() ) { // 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 = DateTime::createFromFormat( $format, $_headers['IF_UNMODIFIED_SINCE'] ); + $check = WP_JSON_DateTime::createFromFormat( $format, $_headers['IF_UNMODIFIED_SINCE'] ); if ( $check !== false ) break; @@ -322,7 +324,7 @@ function editPost( $id, $data, $_headers = array() ) { * @param int $id * @return true on success */ - public function deletePost( $id, $force = false ) { + public function delete_post( $id, $force = false ) { $id = (int) $id; if ( empty( $id ) ) @@ -357,7 +359,7 @@ public function deletePost( $id, $force = false ) { * @param int $id Post ID to retrieve comments for * @return array List of Comment entities */ - public function getComments( $id ) { + public function get_comments( $id ) { //$args = array('status' => $status, 'post_id' => $id, 'offset' => $offset, 'number' => $number )l $comments = get_comments( array('post_id' => $id) ); @@ -374,7 +376,7 @@ public function getComments( $id ) { * @param int $comment Comment ID * @return array Comment entity */ - public function getComment( $comment ) { + public function get_comment( $comment ) { $comment = get_comment( $comment ); $data = $this->prepare_comment( $comment ); return $data; @@ -383,15 +385,15 @@ public function getComment( $comment ) { /** * Get all public post types * - * @uses self::getPostType() + * @uses self::get_post_type() * @return array List of post type data */ - public function getPostTypes() { + public function get_post_types() { $data = get_post_types( array(), 'objects' ); $types = array(); foreach ($data as $name => $type) { - $type = $this->getPostType( $type, true ); + $type = $this->get_post_type( $type, true ); if ( is_wp_error( $type ) ) continue; @@ -408,7 +410,7 @@ public function getPostTypes() { * @param boolean $_in_collection Is this in a collection? (internal use) * @return array Post type data */ - public function getPostType( $type, $_in_collection = false ) { + public function get_post_type( $type, $_in_collection = false ) { if ( ! is_object( $type ) ) $type = get_post_type_object($type); @@ -448,7 +450,7 @@ public function getPostType( $type, $_in_collection = false ) { * * @return array List of post status data */ - public function getPostStatuses() { + public function get_post_statuses() { $statuses = get_post_stati(array(), 'objects'); $data = array(); @@ -495,7 +497,7 @@ protected function prepare_post( $post, $context = 'view' ) { ); $post_type = get_post_type_object( $post['post_type'] ); - if ( ! $this->checkReadPermission( $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 @@ -527,12 +529,12 @@ protected function prepare_post( $post, $context = 'view' ) { // Dates $timezone = $this->server->get_timezone(); - $date = DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_date'], $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 = DateTime::createFromFormat( 'Y-m-d H:i:s', $post['post_modified'], $timezone ); + $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'] ) ); @@ -604,7 +606,11 @@ protected function prepare_excerpt( $excerpt ) { return __( 'There is no excerpt because this is a protected post.' ); } - return apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $excerpt ) ); + $excerpt = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $excerpt ) ); + if ( empty( $excerpt ) ) { + return null; + } + return $excerpt; } /** @@ -632,8 +638,10 @@ protected function prepare_meta( $post_id ) { protected function prepare_author( $author ) { $user = get_user_by( 'id', $author ); - if (!$author) + if (! $author || ! is_object( $user ) ) { return null; + } + $author = array( 'ID' => $user->ID, @@ -887,7 +895,7 @@ protected function parse_date( $date, $force_utc = false ) { if ( strpos( $date, '.' ) !== false ) { $date = preg_replace( '/\.\d+/', '', $date ); } - $datetime = DateTime::createFromFormat( DateTime::RFC3339, $date ); + $datetime = WP_JSON_DateTime::createFromFormat( DateTime::RFC3339, $date ); return $datetime; } @@ -989,7 +997,7 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen // Date $timezone = $this->server->get_timezone(); - $date = DateTime::createFromFormat( 'Y-m-d H:i:s', $comment->comment_date, $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 ) ); @@ -1017,4 +1025,19 @@ protected function prepare_comment( $comment, $requested_fields = array( 'commen return $data; } + + /** + * 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-response.php b/lib/class-wp-json-response.php new file mode 100644 index 0000000000..f3e9f7182c --- /dev/null +++ b/lib/class-wp-json-response.php @@ -0,0 +1,155 @@ +data = $data; + $this->set_status( $status ); + $this->set_headers( $headers ); + } + + /** + * Get headers associated with the response + * + * @return array Map of header name to header value + */ + public function get_headers() { + return $this->headers; + } + + /** + * Set all header values + * + * @param array $headers Map of header name to header value + */ + public function set_headers($headers) { + $this->headers = $headers; + } + + /** + * Set a single HTTP header + * + * @param string $key Header name + * @param string $value Header value + * @param boolean $replace Replace an existing header of the same name? + */ + public function header($key, $value, $replace = true) { + if ( $replace || ! isset( $this->headers[ $key ] ) ) { + $this->headers[ $key ] = $value; + } + else { + $this->headers[ $key ] .= ', ' . $value; + } + } + + /** + * Set a single link header + * + * @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 an IANA 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() ) { + $header = '<' . $link . '>; rel="' . $rel . '"'; + foreach ( $other as $key => $value ) { + if ( 'title' == $key ) + $value = '"' . $value . '"'; + $header .= '; ' . $key . '=' . $value; + } + return $this->header( 'Link', $header, false ); + } + + /** + * Send navigation-related headers for post collections + * + * @param WP_Query $query + */ + public function query_navigation_headers( $query ) { + $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); + } + + /** + * Get the HTTP return code for the response + * + * @return integer 3-digit HTTP status code + */ + public function get_status() { + return $this->status; + } + + /** + * Set the HTTP status code + * + * @param int $code HTTP status + */ + public function set_status( $code ) { + $this->status = absint( $code ); + } + + /** + * Get the response data + * + * @return mixed + */ + public function get_data() { + return $this->data; + } + + /** + * Set the response data + * + * @param mixed $data + */ + public function set_data( $data ) { + $this->data = $data; + } + + /** + * Get the response data for JSON serialization + * + * It is expected that in most implementations, this will return the same as + * {@see get_data()}, however this may be different if you want to do custom + * JSON data handling. + * + * @return mixed Any JSON-serializable value + */ + public function jsonSerialize() { + return $this->get_data(); + } +} diff --git a/lib/class-wp-json-responseinterface.php b/lib/class-wp-json-responseinterface.php new file mode 100644 index 0000000000..001377127d --- /dev/null +++ b/lib/class-wp-json-responseinterface.php @@ -0,0 +1,35 @@ +get_error_data(); + if ( is_array( $error_data ) && isset( $error_data['status'] ) ) { + $status = $error_data['status']; + } + else { + $status = 500; + } + + $data = array(); foreach ( (array) $error->errors as $code => $messages ) { foreach ( (array) $messages as $message ) { - $errors[] = array( 'code' => $code, 'message' => $message ); + $data[] = array( 'code' => $code, 'message' => $message ); } } - return $errors; + $response = new WP_JSON_Response( $data, $status ); + + 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(); } /** @@ -166,7 +193,7 @@ protected function json_error( $code, $message, $status = null ) { * @uses WP_JSON_Server::dispatch() */ public function serve_request( $path = null ) { - $this->header( 'Content-Type', 'application/json; charset=' . get_option( 'blog_charset' ), true ); + $this->send_header( 'Content-Type', 'application/json; charset=' . get_option( 'blog_charset' ), true ); // Proper filter for turning off the JSON API. It is on by default. $enabled = apply_filters( 'json_enabled', true ); @@ -214,13 +241,18 @@ public function serve_request( $path = null ) { $result = $this->dispatch(); } + // Normalize errors to response objects if ( is_wp_error( $result ) ) { - $data = $result->get_error_data(); - if ( is_array( $data ) && isset( $data['status'] ) ) { - $this->send_status( $data['status'] ); - } + $result = $this->error_to_response( $result ); + } + + // Send extra data from response objects + if ( $result instanceof WP_JSON_ResponseInterface ) { + $headers = $result->get_headers(); + $this->send_headers( $headers ); - $result = $this->error_to_array( $result ); + $code = $result->get_status(); + $this->set_status( $code ); } // This is a filter rather than an action, since this is designed to be @@ -257,10 +289,10 @@ public function serve_request( $path = null ) { * * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)` */ - public function getRoutes() { + public function get_routes() { $endpoints = array( // Meta endpoints - '/' => array( array( $this, 'getIndex' ), self::READABLE ), + '/' => array( array( $this, 'get_index' ), self::READABLE ), // Users '/users' => array( @@ -318,7 +350,7 @@ public function dispatch() { default: return new WP_Error( 'json_unsupported_method', __( 'Unsupported request method' ), array( 'status' => 400 ) ); } - foreach ( $this->getRoutes() as $route => $handlers ) { + foreach ( $this->get_routes() as $route => $handlers ) { foreach ( $handlers as $handler ) { $callback = $handler[0]; $supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET; @@ -415,7 +447,7 @@ protected function sort_callback_params( $callback, $provided ) { * * @return array Index entity */ - public function getIndex() { + public function get_index() { // General site data $available = array( 'name' => get_option( 'blogname' ), @@ -431,7 +463,7 @@ public function getIndex() { ); // Find the available routes - foreach ( $this->getRoutes() as $route => $callbacks ) { + foreach ( $this->get_routes() as $route => $callbacks ) { $data = array(); $route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route ); @@ -467,6 +499,17 @@ public function getIndex() { * @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 + * + * @param int $code HTTP status + */ + protected function set_status( $code ) { status_header( $code ); } @@ -478,6 +521,8 @@ public function send_status( $code ) { * @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 @@ -486,6 +531,35 @@ public function header( $key, $value, $replace = true ) { 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): + // 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 ) ); + } + + /** + * Send multiple HTTP headers + * + * @param array Map of header name to header value + */ + protected function send_headers( $headers ) { + foreach ( $headers as $key => $value ) { + $this->send_header( $key, $value ); + } + } + /** * Send a Link header * @@ -500,6 +574,8 @@ public function header( $key, $value, $replace = true ) { * @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 ) @@ -515,6 +591,8 @@ public function link_header( $rel, $link, $other = array() ) { * @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'); @@ -623,7 +701,7 @@ public function parse_date( $date, $force_utc = false ) { if ( strpos( $date, '.' ) !== false ) { $date = preg_replace( '/\.\d+/', '', $date ); } - $datetime = DateTime::createFromFormat( DateTime::RFC3339, $date ); + $datetime = WP_JSON_DateTime::createFromFormat( DateTime::RFC3339, $date, $timezone ); return $datetime; } diff --git a/lib/class-wp-json-taxonomies.php b/lib/class-wp-json-taxonomies.php index 9ba54bbbad..a4424113dd 100644 --- a/lib/class-wp-json-taxonomies.php +++ b/lib/class-wp-json-taxonomies.php @@ -7,20 +7,20 @@ class WP_JSON_Taxonomies { * @param array $routes Existing routes * @return array Modified routes */ - public function registerRoutes( $routes ) { + public function register_routes( $routes ) { $tax_routes = array( '/posts/types/(?P\w+)/taxonomies' => array( - array( array( $this, 'getTaxonomies' ), WP_JSON_Server::READABLE ), + array( array( $this, 'get_taxonomies' ), WP_JSON_Server::READABLE ), ), '/posts/types/(?P\w+)/taxonomies/(?P\w+)' => array( - array( array( $this, 'getTaxonomy' ), 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, 'getTerms' ), WP_JSON_Server::READABLE ), + 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, 'getTerm' ), WP_JSON_Server::READABLE ), + 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 ), ), @@ -30,11 +30,11 @@ public function registerRoutes( $routes ) { /** * Get taxonomies - * + * * @param string $type Post type to get taxonomies for * @return array Taxonomy data */ - public function getTaxonomies( $type ) { + public function get_taxonomies( $type ) { $taxonomies = get_object_taxonomies( $type, 'objects' ); $data = array(); @@ -51,11 +51,11 @@ public function getTaxonomies( $type ) { /** * Get taxonomies - * + * * @param string $type Post type to get taxonomies for * @return array Taxonomy data */ - public function getTaxonomy( $type, $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 ) ); @@ -108,7 +108,7 @@ protected function prepare_taxonomy( $taxonomy, $type, $_in_collection = false ) * @return array Filtered data */ public function add_taxonomy_data( $data, $type ) { - $data['taxonomies'] = $this->getTaxonomies( $type->name ); + $data['taxonomies'] = $this->get_taxonomies( $type->name ); return $data; } @@ -120,7 +120,7 @@ public function add_taxonomy_data( $data, $type ) { * @param string $taxonomy Taxonomy slug * @return array Term collection */ - public function getTerms( $type, $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 ) ); @@ -147,7 +147,7 @@ public function getTerms( $type, $taxonomy ) { * @param string $context Context (view/view-parent) * @return array Term entity */ - public function getTerm( $type, $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 ) ); @@ -201,7 +201,7 @@ protected function prepare_term( $term, $type, $context = 'view' ) { ); if ( ! empty( $data['parent'] ) && $context === 'view' ) { - $data['parent'] = $this->getTerm( $type, $term->taxonomy, $data['parent'], 'view-parent' ); + $data['parent'] = $this->get_term( $type, $term->taxonomy, $data['parent'], 'view-parent' ); } elseif ( empty( $data['parent'] ) ) { $data['parent'] = null; @@ -209,4 +209,19 @@ 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 ); + } + } } diff --git a/lib/wp-json.php b/lib/wp-json.php index 6e6dfde013..1e6b467009 100644 --- a/lib/wp-json.php +++ b/lib/wp-json.php @@ -24,8 +24,8 @@ include('./wp-load.php'); include_once(ABSPATH . 'wp-admin/includes/admin.php'); -include_once(ABSPATH . WPINC . '/class-IXR.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. diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000000..66723a23d1 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,8 @@ + + + + + tests + + + \ No newline at end of file diff --git a/plugin.php b/plugin.php index 4adf7400d1..783505e31a 100644 --- a/plugin.php +++ b/plugin.php @@ -4,12 +4,17 @@ * 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' ); +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-responseinterface.php' ); +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-customposttype.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-pages.php' ); @@ -28,8 +33,8 @@ function json_api_init() { add_action( 'init', 'json_api_init' ); function json_api_register_rewrites() { - add_rewrite_rule( '^wp-json\.php/?$','index.php?json_route=/','top' ); - add_rewrite_rule( '^wp-json\.php(.*)?','index.php?json_route=$matches[1]','top' ); + add_rewrite_rule( '^wp-json/?$','index.php?json_route=/','top' ); + add_rewrite_rule( '^wp-json(.*)?','index.php?json_route=$matches[1]','top' ); } /** @@ -42,24 +47,23 @@ function json_api_default_filters($server) { // Posts $wp_json_posts = new WP_JSON_Posts($server); - add_filter( 'json_endpoints', array( $wp_json_posts, 'registerRoutes' ), 0 ); + add_filter( 'json_endpoints', array( $wp_json_posts, 'register_routes' ), 0 ); // Pages $wp_json_pages = new WP_JSON_Pages($server); - add_filter( 'json_endpoints', array( $wp_json_pages, 'registerRoutes' ), 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); - add_filter( 'json_endpoints', array( $wp_json_media, 'registerRoutes' ), 1 ); - add_filter( 'json_prepare_post', array( $wp_json_media, 'addThumbnailData' ), 10, 3 ); - add_filter( 'json_pre_insert_post', array( $wp_json_media, 'preinsertCheck' ), 10, 3 ); - add_filter( 'json_insert_post', array( $wp_json_media, 'attachThumbnail' ), 10, 3 ); + 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 ); // Posts $wp_json_taxonomies = new WP_JSON_Taxonomies($server); - add_filter( 'json_endpoints', array( $wp_json_taxonomies, 'registerRoutes' ), 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 ); } @@ -67,13 +71,16 @@ function json_api_default_filters($server) { /** * 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 + * help for code re-use by `wp-json` endpoint. Note that we can't unit + * test any method that calls die(). */ function json_api_loaded() { if ( empty( $GLOBALS['wp']->query_vars['json_route'] ) ) return; - include_once( ABSPATH . WPINC . '/class-IXR.php' ); - include_once( ABSPATH . WPINC . '/class-wp-xmlrpc-server.php' ); include_once( dirname( __FILE__ ) . '/lib/class-wp-json-server.php' ); /** @@ -117,19 +124,50 @@ function json_api_loaded() { add_action( 'template_redirect', 'json_api_loaded', -100 ); /** - * Flush the rewrite rules on activation + * Register routes and flush the rewrite rules on activation. */ -function json_api_activation() { - json_api_register_rewrites(); - flush_rewrite_rules(); +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(); + flush_rewrite_rules(); + } + + restore_current_blog(); + + } else { + + json_api_register_rewrites(); + flush_rewrite_rules(); + } } register_activation_hook( __FILE__, 'json_api_activation' ); /** - * Also flush the rewrite rules on deactivation + * Flush the rewrite rules on deactivation */ -function json_api_deactivation() { - flush_rewrite_rules(); +function json_api_deactivation( $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'] ); + flush_rewrite_rules(); + } + + restore_current_blog(); + + } else { + + flush_rewrite_rules(); + } } register_deactivation_hook( __FILE__, 'json_api_deactivation' ); @@ -142,6 +180,45 @@ function json_register_scripts() { } add_action( 'wp_enqueue_scripts', 'json_register_scripts', -100 ); +/** + * Add the API URL to the WP RSD endpoint + */ +function json_output_rsd() { +?> + +\n"; +} +add_action( 'wp_head', 'json_output_link_wp_head', 10, 0 ); + +/** + * Send a Link header for the API + */ +function json_output_link_header() { + if ( headers_sent() ) + return; + + $api_root = get_json_url(); + + if ( empty($api_root) ) + return; + + header('Link: <' . $api_root . '>; rel="https://github.com/WP-API/WP-API"', false); +} +add_action( 'template_redirect', 'json_output_link_header', 11, 0 ); + /** * Get URL to a JSON endpoint on a site * @@ -152,7 +229,7 @@ function json_register_scripts() { * @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.php', $scheme ); + $url = get_home_url( $blog_id, 'wp-json', $scheme ); if ( !empty( $path ) && is_string( $path ) && strpos( $path, '..' ) === false ) $url .= '/' . ltrim( $path, '/' ); diff --git a/testhelper.php b/testhelper.php index 41cb1dff1a..44d72cb7ef 100644 --- a/testhelper.php +++ b/testhelper.php @@ -16,11 +16,11 @@ class WP_JSON_Server_TestHelper { protected $reports = array(); public function __construct() { - add_action('init', array($this, 'startCoverage')); - add_filter('json_endpoints', array($this, 'addEndpoints')); + add_action('init', array($this, 'start_coverage')); + add_filter('json_endpoints', array($this, 'add_endpoints')); } - public function startCoverage() { + public function start_coverage() { if ( ! isset( $_REQUEST['_jsoncurrenttest'] ) ) { return; } @@ -38,7 +38,7 @@ public function startCoverage() { $this->coverage->start( $current_test ); } - public function endCoverage() { + public function end_coverage() { if ( ! $this->coverage ) { return; } @@ -48,14 +48,14 @@ public function endCoverage() { set_transient('json_testhelper_coverage', $this->reports, 30 * MINUTE_IN_SECONDS); } - public function addEndpoints($routes) { + public function add_endpoints($routes) { $routes['/testhelper/report'] = array( - array( array( $this, 'getReports' ), WP_JSON_Server::METHOD_POST ), + array( array( $this, 'get_reports' ), WP_JSON_Server::METHOD_POST ), ); return $routes; } - public function getReports() { + public function get_reports() { $this->reports = get_transient('json_testhelper_coverage'); if (empty($this->reports)) { return new WP_Error('json_testhelper_no_report', __('No report data available', 'json_testhelper'), array('status' => 400)); diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000000..42cd9587a1 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,22 @@ + array( 'WP-API/plugin.php' ), +); + +// 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. + +if( false !== getenv( 'WP_DEVELOP_DIR' ) ) { + require getenv( 'WP_DEVELOP_DIR' ) . '/tests/phpunit/includes/bootstrap.php'; +} else { + require '../../../../tests/phpunit/includes/bootstrap.php'; +} diff --git a/tests/test_json_plugin.php b/tests/test_json_plugin.php new file mode 100644 index 0000000000..b60eb98377 --- /dev/null +++ b/tests/test_json_plugin.php @@ -0,0 +1,37 @@ +assertTrue( is_plugin_active( 'WP-API/plugin.php' ) ); + } + + /** + * The json_api_init hook should have been registered with init, and should + * have a default priority of 10. + */ + function test_init_action_added() { + $this->assertEquals( 10, has_action( 'init', 'json_api_init' ) ); + } + + /** + * The json_route query variable should be registered. + */ + function test_json_route_query_var() { + global $wp; + $this->assertTrue( in_array( 'json_route', $wp->public_query_vars ) ); + } + +} diff --git a/tests/test_json_server.php b/tests/test_json_server.php new file mode 100644 index 0000000000..d67256ceea --- /dev/null +++ b/tests/test_json_server.php @@ -0,0 +1,139 @@ +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. + */ + function test_error_to_array() { + $this->markTestIncomplete('Missing test implementation.'); + } + + /** + * Test the format of errors encoded to json. + */ + function test_json_error() { + $this->markTestIncomplete('Missing test implementation.'); + } + + /** + * The default routes should contain all valid callbacks. This test mostly + * ensures that a set of valid routes have been properly defined. + */ + function test_get_routes() { + // NB: I'd mostly iterate over all endpoints, checking for is_callable(), + // and dispatch() does this check, but that's only at runtime, but + // you could use that as a template for this test. + $this->markTestIncomplete('Missing test implementation.'); + } + + /** + * Ensure the dispatcher calls valid routes with the appropriate method. + */ + function test_dispatch() { + // NB: The dispatcher makes use of get_raw_data() which may not work + // properly with unit tests, so that might need a workaround. + $this->markTestIncomplete('Missing test implementation.'); + } + + /** + * Test sort_callback_params(). + * + * @todo This should probably be broken out into a few unique tests with + * various methods with different reflection properties. + */ + function test_sort_callback_params() { + $this->markTestIncomplete('Missing test implementation.'); + } + + /** + * Test for valid link header format. + * + * @todo This will likely require some changes to $server->header() so it's + * possible to actually write unit tests for headers. + */ + function test_link_header() { + $this->markTestIncomplete('Missing test implementation.'); + } + + /** + * Ensure pagination link headers work properly with valid page counts. + */ + function test_query_navigation_headers() { + $this->markTestIncomplete('Missing test implementation.'); + } + + /** + * Objects passed through prepare_response() should be expanded to arrays. + */ + function test_prepare_response() { + $this->markTestIncomplete('Missing test implementation.'); + } + + /** + * JsonSerializable data passed through prepare_response() should be + * expanded properly. + */ + 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.'); + } + +}