Upload
jeroen-van-dijk
View
286
Download
1
Embed Size (px)
Citation preview
WORDPRESS REST API HACKING
WORDPRESS : IT’S A BLOG
LONG AGO
WORDPRESS : IT’S NOT JUST A BLOG
THE PAST
WORDPRESS : IT’S NOT JUST CMS
PRESENT
WORDPRESS : IT’S AN APPLICATION FRAMEWORK
FUTURE?
BECAUSE OF THE WP REST API
NO TODAY!
RESOURCE BASED STATELESS COMMUNICATION
REPRESENTATIONAL STATE TRANSFER
TO GAIN ACCESS TO (A COMPUTER) ILLEGALLY
TO ALTER (A COMPUTER PROGRAM)
/HĂK/·ING
/00 ALTER THE REST APIWHAT YOU NEED TO KNOW FIRST..
INFRASTRUCTURE IS IN CORE
IMPLEMENTATION IN THE REST-API PLUGIN
2 PARTS
IN CORE
├── rest-api │ ├── class-wp-rest-request.php │ ├── class-wp-rest-response.php │ └── class-wp-rest-server.php ├── rest-api.php
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
DECEMBER 2016
WORDPRESS 4.7
YOU KNOW HOW TO CREATE A PLUGIN?
EXTENDING THE API
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();
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() { }
/01 DISABLE THE API
LET’S START EASY!
public function initServer() { add_filter( 'rest_enabled', [ $this, 'disableApi' ] ); }
public function disableApi( $isEnabled ) { if ( $isEnabled == true ) { return false; } return $isEnabled; }
LET’S START EASY!
public function initServer() { add_filter( 'rest_enabled', [ $this, 'disableApi' ] ); }
public function disableApi( $isEnabled ) { if ( $isEnabled == true ) { return false; } return $isEnabled; }
/02 CHANGE THE ROOT URL
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; }
/03 CHANGE EXPOSURE
register_post_type( $post_type, $args );
register_taxonomy( $taxonomy, $object_type, $args );
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; }
/04 CHANGE CUSTOM POST TYPE HANDLING
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 = 'Enrise\Api\Endpoint\CustomType';
$wp_taxonomies['customtaxonomy']->show_in_rest = true; $wp_taxonomies['customtaxonomy']->rest_base = 'customtaxonomy'; $wp_taxonomies['customtaxonomy']->rest_controller_class = 'Enrise\Api\Endpoint\CustomTaxonomy'; }
class CustomType extends \WP_REST_Posts_Controller {} class CustomTaxonomy extends \WP_REST_Terms_Controller {}
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 ); }
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 ); }
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 );
/05 ROLL YOUR OWN ENDPOINT
ALWAYS ASSUME THERE IS MORE
NAMESPACES
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": { ... } } }
WP_REST_Controller
WP_REST_Posts_Controller
STANDARD IMPLEMENTATION
WP_REST_Controller
Your_Extended_Controller
OPTION #1
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
/06 OVERRIDE DEFAULT FUNCTIONALITY
ANNOTATE AND VALIDATE JSON DOCUMENTS
JSON-SCHEMA.ORG
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
global $wp_rest_additional_fields;
GLOBAL AGAIN…
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
/07 APPLY SOME FILTERING
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
/08 FILE UPLOADS
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
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
/09 CACHE REQUESTS
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…
/10 AUTHENTICATION
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…
IMPLEMENTATIONS
WORDPRESS.ORG/PLUGINS/OAUTH2-PROVIDER
WORDPRESS.ORG/PLUGINS/JWT-AUTHENTICATION-FOR-WP-REST-API
WORDPRESS.ORG/PLUGINS/REST-API-OAUTH1
/11 CHANGE ALL OUTPUT!
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…
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…
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
MORE HOOKS?
V2.WP-API.ORG/EXTENDING/HOOKS
20162016