I’m sharing with you an API that I created for a plugin that I am developing, the code is open source and modify as you want. This should give you a basic idea and get you started.
This API file itself allow me to query the server remotely ofcourse you’ll need a db table to store/verify API access tokens and a class for each method e.g. get users, set role, etc.
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* MC_API class
*/
class MC_API {
private $pretty_print = false;
private $is_valid_request = false;
public $key_data = null;
public $permissions = null;
private $log_requests = true;
private $data = array();
public $endpoint;
public $api_vars;
/**
* Constructor
*/
public function __construct() {
add_action( 'init', array( $this, 'add_endpoint' ) );
add_action( 'wp', array( $this, 'process_query' ), -1 );
add_filter( 'query_vars', array( $this, 'query_vars' ) );
$this->pretty_print = defined( 'JSON_PRETTY_PRINT' ) ? JSON_PRETTY_PRINT : null;
// Allow API request logging to be turned off
$this->log_requests = apply_filters( 'mc_api_log_requests', $this->log_requests );
}
/**
* Add API endpoint
*/
public static function add_endpoint() {
add_rewrite_endpoint( 'mc-api', EP_ALL );
}
/**
* Determines the kind of query requested and also ensure it is a valid query
*/
private function set_query_mode() {
global $wp_query;
// Whitelist our query options
$accepted = apply_filters( 'mc_api_valid_query_modes', array(
'get_user',
'update_user',
'update_usermeta',
'delete_user',
'bulk_delete_users',
'generate_key',
'revoke_key',
'get_permissions',
'set_role',
'set_credits',
'add_credits',
'deduct_credits',
'transfer_credits',
'set_user_status',
'friend_request',
'friend_cancel',
'friend_approve',
'friend_reject',
'friend_delete',
'follow',
'unfollow',
'get_info'
) );
$query = isset( $wp_query->query_vars['mc-api'] ) ? $wp_query->query_vars['mc-api'] : null;
// Make sure our query is valid
if ( ! in_array( $query, $accepted ) ) {
$this->send_error( 'invalid_query' );
}
$this->endpoint = $query;
}
/**
* Registers query vars for API access
*/
public function query_vars( $vars ) {
$this->api_vars = array(
'format',
'consumer_key',
'consumer_secret',
'user',
'users',
'user1',
'user2',
'fields',
'values',
'id',
'limit',
'permissions',
'role',
'amount',
'status'
);
$this->api_vars = apply_filters( 'mc_api_query_vars', $this->api_vars );
foreach( $this->api_vars as $var ) {
$vars[] = $var;
}
return $vars;
}
/**
* Validate the API request
*/
private function validate_request() {
global $wp_query;
$consumer_key = isset( $wp_query->query_vars['consumer_key'] ) ? $wp_query->query_vars['consumer_key'] : null;
$consumer_secret = isset( $wp_query->query_vars['consumer_secret'] ) ? $wp_query->query_vars['consumer_secret'] : null;
if ( ! $consumer_key || ! $consumer_secret ) {
$this->send_error( 'missing_auth' );
}
$user = $this->get_user_by_consumer_key( $consumer_key );
if ( ! $user ) {
$this->send_error( 'invalid_auth', 401 );
}
// Compare provided hash with stored database hash
if ( ! hash_equals( $user->consumer_secret, $consumer_secret ) ) {
$this->send_error( 'invalid_auth', 401 );
}
// Check that user did not exceed API limit
if ( $user->access_limit && $user->queries >= $user->access_limit ) {
$this->send_error( 'exceeded_limit', 401 );
}
/**
* User does not have API manager capability, so we need to ensure that he is querying an endpoint that
* is possible with his API key permissions
*/
$can_read = array(
'get_user',
);
if ( $user->permissions == 'read' && ! in_array( $this->endpoint, $can_read ) ) {
$this->send_error( 'invalid_permissions', 401 );
}
// This is a valid request
$this->is_valid_request = true;
$this->key_data = $user;
$this->update_last_access();
}
/**
* Get user data and API key information by provided consumer key
*/
private function get_user_by_consumer_key( $consumer_key ) {
global $wpdb;
$user = $wpdb->get_row( $wpdb->prepare("SELECT * FROM {$wpdb->prefix}mc_api_keys WHERE consumer_key = %s", $consumer_key ) );
if ( is_object( $user ) ) {
$user->user_id = absint( $user->user_id );
$user->key_id = absint( $user->key_id );
$user->access_limit = absint( $user->access_limit );
$user->queries = absint( $user->queries );
}
return $user;
}
/**
* Updated API Key last access datetime.
*/
private function update_last_access() {
global $wpdb;
$key_id = $this->key_data->key_id;
$queries = $this->key_data->queries + 1;
$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}mc_api_keys SET last_access = %s, queries = %d WHERE key_id = %d", current_time( 'mysql' ), $queries, $key_id ) );
}
/**
* Listens for the API and then processes the API requests
*/
public function process_query() {
global $wp_query;
// Check if user is not querying API
if ( ! isset( $wp_query->query_vars['mc-api'] ) )
return;
// Check for API var. Get out if not present
if ( empty( $wp_query->query_vars['mc-api'] ) ) {
$this->send_error( 'invalid_query' );
}
// Determine the kind of query
$this->set_query_mode();
// Check for a valid user and set errors if necessary
$this->validate_request();
// Only proceed if no errors have been noted
if( ! $this->is_valid_request ) {
$this->send_error( 'invalid_auth', 401 );
}
// Tell WP we are doing API request
if( ! defined( 'MC_DOING_API' ) ) {
define( 'MC_DOING_API', true );
}
// Need to collect $this->data before sending it
$data = array();
$class_name = str_replace(' ', '_', ucwords( str_replace('_', ' ', $this->endpoint ) ) );
$class_name = "MC_API_" . $class_name;
$data = new $class_name();
}
/**
* Before we send the data to output function
*/
public function send_data( $data ) {
global $wp_query, $wpdb;
$this->data = apply_filters( 'mc_api_output_data', $data, $this->endpoint, $this );
// In case we do not have any data even after filtering
if ( count( (array) $this->data ) == 0 ) {
$this->data = array(
'message' => __( 'Your API request returned no data.', 'mojocommunity' ),
'queried_endpoint' => $this->endpoint
);
}
// Log this API request
$this->log_request();
$this->output();
}
/**
* Log a successful API request
*/
private function log_request() {
global $wp_query;
if ( ! $this->log_requests )
return;
$log = new MC_API_Log();
}
/**
* The query data is outputted as JSON by default
*/
private function output( $status_code = 200 ) {
$format = $this->get_output_format();
status_header( $status_code );
do_action( 'mc_api_output_before', $this->data, $this, $format );
switch ( $format ) :
case 'json' :
header( 'Content-Type: application/json' );
if ( ! empty( $this->pretty_print ) )
echo json_encode( $this->data, $this->pretty_print );
else
echo json_encode( $this->data );
break;
default :
// Allow other formats to be added via extensions
do_action( 'mc_api_output_' . $format, $this->data, $this );
break;
endswitch;
do_action( 'mc_api_output_after', $this->data, $this, $format );
die();
}
/**
* Generate API key.
*/
public function generate_api_key( $args = array() ) {
global $wpdb;
$user_id = ( isset( $args['user_id'] ) ) ? absint( $args['user_id'] ) : null;
$description = ( isset( $args['description'] ) ) ? $args['description'] : __('Generated via the API', 'mojocommunity' );
$permissions = ( isset( $args['permissions'] ) && in_array( $args['permissions'], array( 'read', 'write', 'read_write' ) ) ) ? $args['permissions'] : 'read';
$access_limit = ( isset( $args['access_limit'] ) ) ? absint( $args['access_limit'] ) : 0;
$consumer_key = 'ck_' . mc_rand_hash();
$consumer_secret="cs_" . mc_rand_hash();
$queries = 0;
if ( ! $user_id )
return false;
$data = array(
'user_id' => $user_id,
'description' => $description,
'permissions' => $permissions,
'consumer_key' => $consumer_key,
'consumer_secret' => $consumer_secret,
'truncated_key' => substr( $consumer_key, -7 ),
'access_limit' => $access_limit,
'queries' => $queries
);
$wpdb->insert(
$wpdb->prefix . 'mc_api_keys',
$data,
array(
'%d',
'%s',
'%s',
'%s',
'%s',
'%s',
'%d',
'%d'
)
);
$data = array(
'user_id' => $user_id,
'consumer_key' => $consumer_key,
'consumer_secret' => $consumer_secret,
'permissions' => $permissions,
'access_limit' => $access_limit
);
return $data;
}
/**
* Revokes API access
*/
public function revoke_api_key( $key_id = 0 ) {
global $wpdb;
$key = $wpdb->get_row( $wpdb->prepare( "SELECT user_id, truncated_key FROM {$wpdb->prefix}mc_api_keys WHERE key_id = %d;", $key_id ) );
if ( ! $key ) {
return new MC_Error( 'invalid_api_key_id', __( 'The specified API key identifier is invalid.', 'mojocommunity' ) );
}
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}mc_api_keys WHERE key_id = %d;", $key->key_id ) );
$data = array(
'user_id' => $key->user_id,
'consumer_key_ending_in' => $key->truncated_key,
'success' => __( 'The API access has been revoked.', 'mojocommunity' )
);
return $data;
}
/**
* Retrieve the output format
*/
private function get_output_format() {
global $wp_query;
$format = isset( $wp_query->query_vars['format'] ) ? $wp_query->query_vars['format'] : 'json';
return apply_filters( 'mc_api_output_format', $format );
}
/**
* Returns a customized error message for the API query
*/
public function send_error( $error="", $code = 400 ) {
switch( $error ) {
default :
break;
case 'invalid_query' :
$error = __( 'The requested API method is invalid or missing parameters.', 'mojocommunity' );
break;
case 'missing_auth' :
$error = __( 'Your API request could not be authenticated due to missing credentials.', 'mojocommunity' );
break;
case 'invalid_auth' :
$error = __( 'Your API request could not be authenticated due to invalid credentials.', 'mojocommunity' );
break;
case 'exceeded_limit' :
$error = __( 'You have exceeded your API usage limit for this key.', 'mojocommunity' );
break;
case 'invalid_permissions' :
$error = __( 'Your API request could not be authenticated due to invalid permissions.', 'mojocommunity' );
break;
}
$this->data = array(
'error' => $error,
'error_code' => $code
);
$this->output( $code );
}
}
To compliment the above code, here’s a sample API call/class let’s say get_users query/endpoint.
/**
* MC_API_Get_User class
*/
class MC_API_Get_User {
/**
* Constructor
*/
public function __construct() {
global $wpdb, $wp_query;
$api = MC()->api;
$user = ( isset( $wp_query->query_vars['user'] ) ) ? $wp_query->query_vars['user'] : null;
$fields = ( isset( $wp_query->query_vars['fields'] ) ) ? $wp_query->query_vars['fields'] : null;
if ( ! $user ) {
$api->send_error( 'invalid_query' );
}
$data = new MC_User( $user, $fields );
$api->send_data( $data );
}
}
Please note how I use $api->send_data() method which is defined in original API to send/output the data as json. I forgot to say that you need
- A table with api keys, access limit, assigned user (the basic stuff)
- A custom post type if you’re willing to log the request details (always good to know who’s sneaking in your api
The above code does multiple verifications which are
-
Ensures that the user provides true valid public and secret keys.
-
Validates that the API key access limit is not hit
-
Ensure that the API key has the permission (read/write) to do the
specified task. For example, you can setup a readonly API key that
can just call your public methods e.g. get user information and make
another key for updating/editing that have read and write
permissions.
Here’s the database table structure I use for storing API keys hopefully will save you some time doing your own:
CREATE TABLE {$wpdb->prefix}mc_api_keys (
key_id bigint(20) NOT NULL auto_increment,
user_id bigint(20) NOT NULL,
access_limit bigint(20) NOT NULL DEFAULT 0,
queries bigint(20) NOT NULL DEFAULT 0,
description longtext NULL,
permissions varchar(10) NOT NULL,
consumer_key char(64) NOT NULL,
consumer_secret char(64) NOT NULL,
truncated_key char(7) NOT NULL,
last_access datetime NULL DEFAULT null,
PRIMARY KEY (key_id),
KEY consumer_key (consumer_key),
KEY consumer_secret (consumer_secret)
) $collate;
And here is the logger class. Responsible for inserting each request as a log in the database.
/**
* MC_API_Log class
*/
class MC_API_Log {
protected $api = null;
public $log_id = 0;
/**
* Constructor
*/
public function __construct( $log_id = 0 ) {
$this->log_id = $log_id;
if ( $this->log_id > 0 ) {
$this->init_meta();
} else {
$this->api = MC()->api;
$this->send();
}
}
/**
* Init all post meta
*/
public function init_meta() {
$meta = get_post_meta( $this->log_id );
$this->consumer_key = ( isset( $meta['consumer_key'][0] ) ) ? $meta['consumer_key'][0] : null;
$this->key_id = ( isset( $meta['key_id'][0] ) ) ? absint( $meta['key_id'][0] ) : null;
$this->user_id = ( isset( $meta['user_id'][0] ) ) ? absint( $meta['user_id'][0] ) : 0;
$this->endpoint = ( isset( $meta['endpoint'][0] ) ) ? $meta['endpoint'][0] : null;
$this->user_ip = ( isset( $meta['user_ip'][0] ) ) ? $meta['user_ip'][0] : '127.0.0.1';
$this->time = get_the_time( 'j M Y g:ia', $this->log_id );
}
/**
* Get truncated key
*/
public function get_key_html() {
return ( $this->consumer_key ) ? '<a href="#">' . '…' . esc_html( substr( $this->consumer_key, -7 ) ) . '</a>' : __( 'Invalid key', 'mojocommunity' );
}
/**
* Get user html
*/
public function get_user_html() {
$userdata = get_userdata( $this->user_id );
if ( false === $userdata ) {
return '';
}
return '<a href="' . get_edit_user_link( $this->user_id ) . '">' . $userdata->user_login . '</a>';
}
/**
* Get endpoint html
*/
public function get_endpoint_html() {
return ( $this->endpoint ) ? '<code>' . $this->endpoint . '</code>' : null;
}
/**
* Get ID
*/
public function get_id() {
return $this->log_id;
}
/**
* Get IP
*/
public function get_ip() {
return $this->user_ip;
}
/**
* Get date/time of a request
*/
public function get_time() {
return $this->time;
}
/**
* Insert the log
*/
public function insert_log() {
global $wp_query;
if ( ! empty( $this->error ) )
return;
$query = array();
foreach( $this->api->api_vars as $var ) {
if ( isset( $wp_query->query_vars[ $var ] ) && ! empty( $wp_query->query_vars[ $var ] ) && ! in_array( $var, array( 'consumer_key', 'consumer_secret' ) ) ) {
$query[ $var ] = $wp_query->query_vars[ $var ];
}
}
if ( http_build_query( $query ) ) {
$query = '?' . http_build_query( $query );
} else {
$query = null;
}
$this->post_args = apply_filters( 'mc_new_api_request_args', array(
'post_author' => absint( $this->api->key_data->user_id ),
'post_status' => 'publish',
'post_type' => 'log',
'comment_status' => 'closed'
) );
$this->post_meta = apply_filters( 'mc_new_api_request_meta', array(
'key_id' => absint( $this->api->key_data->key_id ),
'user_id' => absint( $this->api->key_data->user_id ),
'consumer_key' => $this->api->key_data->consumer_key,
'user_ip' => mc_get_ip(),
'endpoint' => $this->api->endpoint . $query
) );
do_action( 'mc_before_insert_api_log', $this->post_meta );
$this->log_id = mc_insert_post( $this->post_args, $this->post_meta );
do_action( 'mc_after_insert_api_log', $this->log_id, $this->post_meta );
}
/**
* Send the log
*/
public function send() {
$this->insert_log();
}
}
Here’s the mc_insert_post
function I use to insert logs (or other post types)
/**
* A wrapper function for inserting posts in database
*/
function mc_insert_post( $postarr = array(), $meta_input = array() ) {
do_action( 'mc_before_insert_post', $postarr, $meta_input );
if ( ! empty( $meta_input ) && is_array( $meta_input ) )
$postarr = array_merge( $postarr, array( 'meta_input' => $meta_input ) );
$post_id = wp_insert_post( $postarr );
do_action( 'mc_after_insert_post', $post_id, $postarr, $meta_input );
return $post_id;
}