Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

WordPress REST API hacking

1,356 views

Published on

The REST API is an awesome plugin to expose your data from the WordPress core. But … the standard implementation might not fit your specific case.

Just like the WordPress core, you'll be able to extend it to your specific needs. I'll show you how to handle authentication, introduce caching strategies, alter custom post types, or even change the default way of communication altogether.

Published in: Technology
  • Be the first to like this

WordPress REST API hacking

  1. 1. WORDPRESS REST API HACKING
  2. 2. WORDPRESS : IT’S A BLOG LONG AGO
  3. 3. WORDPRESS : IT’S NOT JUST A BLOG THE PAST
  4. 4. WORDPRESS : IT’S NOT JUST CMS PRESENT
  5. 5. WORDPRESS : IT’S AN APPLICATION FRAMEWORK FUTURE?
  6. 6. BECAUSE OF THE WP REST API NO TODAY!
  7. 7. RESOURCE BASED STATELESS COMMUNICATION REPRESENTATIONAL STATE TRANSFER
  8. 8. TO GAIN ACCESS TO (A COMPUTER) ILLEGALLY TO ALTER (A COMPUTER PROGRAM) /HĂK/·ING
  9. 9. /00 ALTER THE REST API WHAT YOU NEED TO KNOW FIRST..
  10. 10. INFRASTRUCTURE IS IN CORE IMPLEMENTATION IN THE REST-API PLUGIN 2 PARTS
  11. 11. IN CORE ├── rest-api │   ├── class-wp-rest-request.php │   ├── class-wp-rest-response.php │   └── class-wp-rest-server.php ├── rest-api.php
  12. 12. IN PLUGIN ├── class-wp-rest-attachments-controller.php ├── class-wp-rest-comments-controller.php ├── class-wp-rest-controller.php ├── class-wp-rest-post-statuses-controller.php ├── class-wp-rest-post-types-controller.php ├── class-wp-rest-posts-controller.php ├── class-wp-rest-revisions-controller.php ├── class-wp-rest-settings-controller.php ├── class-wp-rest-taxonomies-controller.php ├── class-wp-rest-terms-controller.php └── class-wp-rest-users-controller.php
  13. 13. DECEMBER 2016 WORDPRESS 4.7
  14. 14. YOU KNOW HOW TO CREATE A PLUGIN? EXTENDING THE API
  15. 15. MY PLUGIN.PHP SETUP <?php /* Plugin Name: My REST API extension Plugin URI: https://www.a-wp-site.com/ Description: WordPress REST API extension Version: 1.0.0 Author: Enrise Author URI: https://www.enrise.com */ require_once('src/Bootstrap.php'); new Bootstrap::getInstance();
  16. 16. WHEN TO TRIGGER YOUR CODE public static function getInstance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); } return self::$instance; } protected function __construct() { add_action( 'plugins_loaded', [ $this, 'initServer' ], 100 ); } public function initServer() { }
  17. 17. /01 DISABLE THE API
  18. 18. LET’S START EASY! public function initServer() { add_filter( 'rest_enabled', [ $this, 'disableApi' ] ); } public function disableApi( $isEnabled ) { if ( $isEnabled == true ) { return false; } return $isEnabled; }
  19. 19. LET’S START EASY! public function initServer() { add_filter( 'rest_enabled', [ $this, 'disableApi' ] ); } public function disableApi( $isEnabled ) { if ( $isEnabled == true ) { return false; } return $isEnabled; }
  20. 20. /02 CHANGE THE ROOT URL
  21. 21. CHANGE WP-JSON INTO … public function initServer() { if ( ! defined( 'REST_API_VERSION' ) ) { // return early if WP API versions do not exist add_action( 'all_admin_notices', [ $this, 'showError' ] ); return; } add_filter( 'rest_url_prefix', [ $this, 'changeApiBase' ] ); } public function changeApiBase( $prefix ) { if ($prefix === 'wp-json') { return 'api'; } return $prefix; }
  22. 22. /03 CHANGE EXPOSURE
  23. 23. register_post_type( $post_type, $args ); register_taxonomy( $taxonomy, $object_type, $args );
  24. 24. ACCESS POST TYPE & TAXONOMY public function initServer() { add_action( 'rest_api_init', [ $this, 'updateExposure' ], 12 ); } public function updateExposure() { global $wp_post_types, $wp_taxonomies; $wp_post_types['customposttype']->show_in_rest = true; $wp_taxonomies['customtaxonomy']->show_in_rest = true; }
  25. 25. /04 CHANGE CUSTOM POST TYPE HANDLING
  26. 26. EXTEND API CONTROLLERS public function updateExposure() { global $wp_post_types, $wp_taxonomies; $wp_post_types['customposttype']->show_in_rest = true; $wp_post_types['customposttype']->rest_base = 'customposttype'; $wp_post_types['customposttype']->rest_controller_class = 'EnriseApiEndpointCustomType'; $wp_taxonomies['customtaxonomy']->show_in_rest = true; $wp_taxonomies['customtaxonomy']->rest_base = 'customtaxonomy'; $wp_taxonomies['customtaxonomy']->rest_controller_class = 'EnriseApiEndpointCustomTaxonomy'; } class CustomType extends WP_REST_Posts_Controller {} class CustomTaxonomy extends WP_REST_Terms_Controller {}
  27. 27. EXTEND THE INPUT / OUTPUT register_rest_field( 'customposttype', 'custommetafield', [ 'schema' => [ 'type' => 'integer', 'context' => [ 'view', 'edit' ], ], 'get_callback' => [ $this, 'getMetaField' ], 'update_callback' => [ $this, 'saveMetaField' ] ] ); public function getMetaField( $post, $key, $request ) { return get_post_meta( $post['id'], $key, true ); } public function saveMetaField( $value, $post, $key ) { return update_post_meta( $post->ID, $key, $value ); }
  28. 28. EXTEND THE INPUT / OUTPUT register_rest_field( 'customposttype', 'custommetafield', [ 'schema' => [ 'type' => 'integer', 'context' => [ 'view', 'edit' ], ], 'get_callback' => [ $this, 'getMetaField' ], 'update_callback' => [ $this, 'saveMetaField' ] ] ); public function getMetaField( $post, $key, $request ) { return get_post_meta( $post['id'], $key, true ); } public function saveMetaField( $value, $post, $key ) { return update_post_meta( $post->ID, $key, $value ); }
  29. 29. REST API 2.0 BETA 15 $args =[ 'sanitize_callback' => 'sanitize_my_meta_key', 'auth_callback' => 'authorize_my_meta_key', 'type' => 'string', 'description' => 'My registered meta key', 'single' => true, 'show_in_rest' => true, ]; register_meta( 'post', 'my_meta_key', $args );
  30. 30. /05 ROLL YOUR OWN ENDPOINT
  31. 31. ALWAYS ASSUME THERE IS MORE NAMESPACES
  32. 32. NAMESPACES { "name": "A WP Site", "description": "", "URL": "https://www.a-wp-site.com", "namespaces": [ "wp/v2", "your_namespace" ], "authentication": [] "routes": { "/": { ... }, "/wp/v2/posts": { ... }, ... "/your_namespace/path": { ... } } }
  33. 33. WP_REST_Controller WP_REST_Posts_Controller STANDARD IMPLEMENTATION
  34. 34. WP_REST_Controller Your_Extended_Controller OPTION #1
  35. 35. register_rest_route( 'enrise', '/version/(?P<os>[a-z]{3,7})', [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'getVersion' ], 'permission_callback' => [ $this, 'checkAccess' ], 'args' => [ 'os' => [ 'validate_callback' => [ $this, 'isOS' ], 'sanitize_callback' => [ $this, 'filterOS' ], 'required' => true, ], ], ], ] ); OPTION #2
  36. 36. /06 OVERRIDE DEFAULT FUNCTIONALITY
  37. 37. ANNOTATE AND VALIDATE JSON DOCUMENTS JSON-SCHEMA.ORG
  38. 38. public function get_item_schema() { $schema = [ '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', /* * Base properties for every Post. */ 'properties' => [ 'date' => [ 'description' => __( "The date the object was published, in the site's timezone." ), 'type' => 'string', 'format' => 'date-time', 'context' => [ 'view', 'edit', 'embed' ], ], ... ]; } JSON SCHEMA DEFINITION
  39. 39. global $wp_rest_additional_fields; GLOBAL AGAIN…
  40. 40. register_rest_field( 'user', 'first_name', [ 'schema' => [ 'description' => __( 'First name for the resource.' ), 'type' => 'string', 'context' => [ 'edit', 'embed', 'view' ], 'arg_options' => [ 'sanitize_callback' => 'sanitize_text_field', ], ], ] ); register_rest_field( 'user', 'last_name', [ 'schema' => [ 'description' => __( 'Last name for the resource.' ), 'type' => 'string', 'context' => [ 'edit', 'embed', 'view' ], 'arg_options' => [ 'sanitize_callback' => 'sanitize_text_field', ], ], ] ); OVERRIDE DEFAULT CONFIGURATION
  41. 41. /07 APPLY SOME FILTERING
  42. 42. public function initServer() { add_filter('rest_customposttype_query', [ $this, 'filterbyMetaField' ], 10, 2); } /** * @param $args * @param $request WP_REST_Request */ public function filterByMetaField($args, $request) { $filter = []; $filter['meta_key'] = 'custommetafield'; $filter['meta_value'] = 1476896350; $filter['meta_type'] = 'DECIMAL'; $filter['meta_compare'] = '>='; return array_merge($args, $filter); } CHANGE THE QUERY BEHAVIOUR
  43. 43. /08 FILE UPLOADS
  44. 44. xhr.onload = function() { var attachment = JSON.parse(this.responseText); $.current.set('featured_image', attachment.id); saveCustompost(); }; xhr.open('POST', '/wp-json/wp/v2/media'); xhr.setRequestHeader('Content-Type', 'image/jpeg'); xhr.setRequestHeader('Content-Disposition', 'attachment; filename=filename.jpg'); xhr.send(image.read()); UPLOAD A FILE
  45. 45. public function initServer() { add_filter('wp_handle_sideload_prefilter', [ $this, 'autoRotate' ]); add_filter('rest_insert_customposttype', [$this, 'saveAttachment'], 10, 3); } /** * @param $post WP_Post * @param $request WP_REST_Request * @param $create bool */ public function saveAttachment($post, $request, $create) { if ($create === true && isset( $request['featured_image'] )) { set_post_thumbnail( $post->ID, $request['featured_image'] ); } } HANDLE THE ATTACHMENT
  46. 46. /09 CACHE REQUESTS
  47. 47. add_filter( 'rest_pre_dispatch', [ $this, 'lastUpdate' ], 10, 3 ); public function lastUpdate( $response, $server, $request ) { $since = $request->get_header( 'if_modified_since' ); if ( $since === null ) { return $response; } if ( $response !== null || $response->get_route() !== '/wp/v2/customposttype' ) { return $response; } $lastrequest = DateTime::createFromFormat( DateTime::RFC1123, $since ); $lastpost = DateTime::createFromFormat( 'Y-m-d H:i:s', get_lastpostmodified( 'gmt', 'customposttype' ) ); if ( $lastrequest >= $lastpost ) { return new WP_REST_Response( null, 304 ); } return $response; } LET THE CLIENT CACHE…
  48. 48. /10 AUTHENTICATION
  49. 49. add_filter( 'determine_current_user', [ $this, 'authenticate' ] ); add_filter( 'rest_authentication_errors', [ $this, 'getErrors' ] ); public function authenticate( $user ) { | public function getErrors( $value ) { // method run/used multiple times | // already overrided if ($user instanceof WP_User) { | if ( $value !== null ) { return $user; | return $value; } | } // roll your own authentication here | // WP_Error|null|bool(!?) if (false) { | return $this->status; $this->status = new WP_Error(); | } return null; | } | $this->status = true; | return $user->ID; | } | TRY TO DEBUG THIS…
  50. 50. IMPLEMENTATIONS WORDPRESS.ORG/PLUGINS/OAUTH2-PROVIDER WORDPRESS.ORG/PLUGINS/JWT-AUTHENTICATION-FOR-WP-REST-API WORDPRESS.ORG/PLUGINS/REST-API-OAUTH1
  51. 51. /11 CHANGE ALL OUTPUT!
  52. 52. add_filter( 'rest_pre_serve_request', [ $this, 'changeResponse' ], 10, 4 ); public function changeResponse( $served, $response, $request, $server ) { $route = $response->get_matched_route(); if ( $served || $route !== '/wp/v2/customposttype' ) { return false; } if ( 'HEAD' === $request->get_method() ) { return null; } $result = $server->response_to_data( $response, true ); $transform = new Transformer( $result ); echo wp_json_encode( iterator_to_array( $transform ) ); return true; } THIS SHOULDN’T BE NECESSARY…
  53. 53. class Transformer extends ArrayIterator { public function current() { $item = parent::current(); $item = $this->modify( $item ); return $item; } private function modify( array $data ) { // modify the data in any way return $data; } } THIS SHOULDN’T BE NECESSARY…
  54. 54. add_filter('rest_prepare_customposttype', 'beforeOutput', 10, 3); add_filter('rest_pre_insert_customposttype', 'beforeInsertInDb', 10, 2); add_filter('rest_insert_customposttype', 'afterInsertInDb', 10, 3); add_filter('rest_query_vars', 'filterQueryVars', 10, 1); OTHER INTERESTING HOOKS
  55. 55. MORE HOOKS? V2.WP-API.ORG/EXTENDING/HOOKS
  56. 56. 20162016

×