diff --git a/admin.php b/admin.php index d04bbea..b5309c6 100644 --- a/admin.php +++ b/admin.php @@ -3,276 +3,18 @@ * Administration UI and utilities */ -add_action( 'admin_menu', 'json_oauth_admin_register' ); -add_action( 'admin_init', 'json_oauth_admin_prerender' ); +require dirname( __FILE__ ) . '/lib/class-wp-rest-oauth1-admin.php'; -add_action( 'admin_action_json-oauth-add', 'json_oauth_admin_edit_page' ); -add_action( 'admin_action_json-oauth-edit', 'json_oauth_admin_edit_page' ); +add_action( 'admin_menu', array( 'WP_REST_OAuth1_Admin', 'register' ) ); -add_action( 'personal_options', 'json_oauth_profile_section', 50 ); +add_action( 'personal_options', 'rest_oauth1_profile_section', 50 ); -add_action( 'all_admin_notices', 'json_oauth_profile_messages' ); +add_action( 'all_admin_notices', 'rest_oauth1_profile_messages' ); -add_action( 'personal_options_update', 'json_oauth_profile_save', 10, 1 ); -add_action( 'edit_user_profile_update', 'json_oauth_profile_save', 10, 1 ); +add_action( 'personal_options_update', 'rest_oauth1_profile_save', 10, 1 ); +add_action( 'edit_user_profile_update', 'rest_oauth1_profile_save', 10, 1 ); -/** - * Register the admin page - */ -function json_oauth_admin_register() { - /** - * Include anything we need that relies on admin classes/functions - */ - include_once dirname( __FILE__ ) . '/lib/class-wp-json-authentication-oauth1-listtable.php'; - - add_users_page( - // Page title - __( 'Registered OAuth Applications', 'json_oauth' ), - - // Menu title - _x( 'Applications', 'menu title', 'json_oauth' ), - - // Capability - 'list_users', - - // Menu slug - 'json-oauth', - - // Callback - 'json_oauth_admin_render' - ); -} - -function json_oauth_admin_prerender() { - $hook = get_plugin_page_hook( 'json-oauth', 'users.php' ); - - add_action( 'load-' . $hook, 'json_oauth_admin_load' ); -} - -function json_oauth_admin_load() { - global $wp_list_table; - - $wp_list_table = new WP_JSON_Authentication_OAuth1_ListTable(); - - $wp_list_table->prepare_items(); -} - -function json_oauth_admin_render() { - global $wp_list_table; - - // ... - ?> -
-

- - - -

- - views(); ?> - -
- - search_box( __( 'Search Applications', 'json_oauth' ), 'json_oauth' ); ?> - - display(); ?> - -
- -
- -
- ID ); - } - - // Check that the parameters are correct first - $params = json_oauth_admin_validate_parameters( wp_unslash( $_POST ) ); - if ( is_wp_error( $params ) ) { - $messages[] = $params->get_error_message(); - return $messages; - } - - if ( empty( $consumer ) ) { - $authenticator = new WP_JSON_Authentication_OAuth1(); - - // Create the consumer - $data = array( - 'name' => $params['name'], - 'description' => $params['description'], - ); - $consumer = $result = $authenticator->add_consumer( $data ); - } - else { - // Update the existing consumer post - $data = array( - 'ID' => $consumer->ID, - 'post_title' => $params['name'], - 'post_content' => $params['description'], - ); - $result = wp_update_post( $data, true ); - } - - if ( is_wp_error( $result ) ) { - $messages[] = $result->get_error_message(); - - return $messages; - } - - // Success, redirect to alias page - $location = add_query_arg( - array( - 'action' => 'json-oauth-edit', - 'id' => $consumer->ID, - 'did_action' => $did_action, - 'processed' => 1, - '_wpnonce' => wp_create_nonce( 'json-oauth-edit-' . $id ), - ), - network_admin_url( 'admin.php' ) - ); - wp_safe_redirect( $location ); - exit; -} - -/** - * Output alias editing page - */ -function json_oauth_admin_edit_page() { - if ( ! current_user_can( 'edit_users' ) ) - wp_die( __( 'You do not have permission to access this page.' ) ); - - // Are we editing? - $consumer = null; - $form_action = admin_url( 'admin.php?action=json-oauth-add' ); - if ( ! empty( $_REQUEST['id'] ) ) { - $id = absint( $_REQUEST['id'] ); - $consumer = get_post( $id ); - if ( is_wp_error( $consumer ) || empty( $consumer ) ) { - wp_die( __( 'Invalid consumer ID.' ) ); - } - - $form_action = admin_url( 'admin.php?action=json-oauth-edit' ); - } - - // Handle form submission - $messages = array(); - if ( ! empty( $_POST['submit'] ) ) { - $messages = json_oauth_admin_handle_edit_submit( $consumer ); - } - - $data = array(); - - if ( empty( $consumer ) || ! empty( $_POST['_wpnonce'] ) ) { - foreach ( array( 'name', 'description' ) as $key ) { - $data[ $key ] = empty( $_POST[ $key ] ) ? '' : wp_unslash( $_POST[ $key ] ); - } - } - else { - $data['name'] = $consumer->post_title; - $data['description'] = $consumer->post_content; - } - - // Header time! - global $title, $parent_file, $submenu_file; - $title = $consumer ? __( 'Edit Consumer' ) : __( 'Add Consumer' ); - $parent_file = 'users.php'; - $submenu_file = 'json-oauth'; - - include( ABSPATH . 'wp-admin/admin-header.php' ); -?> - -
-

- -

' . $msg . '

'; - } - ?> - -
- - - - - - - - - -
- - - -
- - - -
- - ID ) . '" />'; - wp_nonce_field( 'json-oauth-edit-' . $consumer->ID ); - submit_button( __( 'Save Consumer' ) ); - } - - ?> -
- - -get_col( "SELECT option_value FROM {$wpdb->options} WHERE option_name LIKE 'oauth1_access_%'", 0 ); @@ -281,36 +23,37 @@ function json_oauth_profile_section( $user ) { return $row['user'] === $user->ID; } ); - $authenticator = new WP_JSON_Authentication_OAuth1(); + $authenticator = new WP_REST_OAuth1(); ?> - + @@ -319,35 +62,35 @@ function json_oauth_profile_section( $user ) {

' . __( 'Token revoked.' ) . '

'; + if ( ! empty( $_GET['rest_oauth1_revoked'] ) ) { + echo '

' . __( 'Token revoked.', 'rest_oauth1' ) . '

'; } - if ( ! empty( $_GET['oauth_revocation_failed'] ) ) { - echo '

' . __( 'Unable to revoke token.' ) . '

'; + if ( ! empty( $_GET['rest_oauth1_revocation_failed'] ) ) { + echo '

' . __( 'Unable to revoke token.', 'rest_oauth1' ) . '

'; } } -function json_oauth_profile_save( $user_id ) { - if ( empty( $_POST['oauth_revoke'] ) ) { +function rest_oauth1_profile_save( $user_id ) { + if ( empty( $_POST['rest_oauth1_revoke'] ) ) { return; } - $key = wp_unslash( $_POST['oauth_revoke'] ); + $key = wp_unslash( $_POST['rest_oauth1_revoke'] ); - $authenticator = new WP_JSON_Authentication_OAuth1(); + $authenticator = new WP_REST_OAuth1(); $result = $authenticator->revoke_access_token( $key ); if ( is_wp_error( $result ) ) { - $redirect = add_query_arg( 'oauth_revocation_failed', true, get_edit_user_link( $user_id ) ); + $redirect = add_query_arg( 'rest_oauth1_revocation_failed', true, get_edit_user_link( $user_id ) ); } else { - $redirect = add_query_arg( 'oauth_revoked', $key, get_edit_user_link( $user_id ) ); + $redirect = add_query_arg( 'rest_oauth1_revoked', $key, get_edit_user_link( $user_id ) ); } wp_redirect($redirect); exit; diff --git a/lib/class-wp-json-authentication.php b/lib/class-wp-json-authentication.php deleted file mode 100644 index 615de8b..0000000 --- a/lib/class-wp-json-authentication.php +++ /dev/null @@ -1,92 +0,0 @@ -type ) ) { - _doing_it_wrong( 'WP_JSON_Authentication::__construct', __( 'The type of authentication must be set' ), 'WPAPI-0.9' ); - return; - } - - add_filter( 'json_check_authentication', array( $this, 'authenticate' ), 0 ); - add_filter( 'rest_authentication_errors', array( $this, 'get_authentication_errors' ), 0 ); - } - - abstract public function authenticate( $user ); - - abstract public function get_authentication_errors( $value ); - - public function get_consumer( $key ) { - $this->should_attempt = false; - - $query = new WP_Query(); - $consumers = $query->query( array( - 'post_type' => 'json_consumer', - 'post_status' => 'any', - 'meta_query' => array( - array( - 'meta_key' => 'key', - 'meta_value' => $key, - ), - array( - 'meta_key' => 'type', - 'meta_value' => $this->type, - ), - ), - ) ); - - $this->should_attempt = true; - - if ( empty( $consumers ) || empty( $consumers[0] ) ) - return new WP_Error( 'json_consumer_notfound', __( 'Consumer Key is invalid' ), array( 'status' => 401 ) ); - - return $consumers[0]; - } - - public function add_consumer( $params ) { - $default = array( - 'name' => '', - 'description' => '', - 'meta' => array(), - ); - $params = wp_parse_args( $params, $default ); - - $data = array( - 'post_type' => 'json_consumer', - 'post_title' => $params['name'], - 'post_content' => $params['description'], - ); - - $ID = wp_insert_post( $data ); - if ( is_wp_error( $ID ) ) { - return $ID; - } - - $meta = $params['meta']; - $meta['type'] = $this->type; - $meta = apply_filters( 'json_consumer_meta', $meta, $ID, $params ); - - foreach ( $meta as $key => $value ) { - update_post_meta( $ID, $key, $value ); - } - - return get_post( $ID ); - } -} diff --git a/lib/class-wp-rest-client.php b/lib/class-wp-rest-client.php new file mode 100644 index 0000000..2165de5 --- /dev/null +++ b/lib/class-wp-rest-client.php @@ -0,0 +1,242 @@ +post = $post; + } + + /** + * Getter. + * + * Passes through to post object. + * + * @param string $key Key to get. + * @return mixed + */ + public function __get( $key ) { + return $this->post->$key; + } + + /** + * Isset-er. + * + * Passes through to post object. + * + * @param string $key Property to check if set. + * @return bool + */ + public function __isset( $key ) { + return isset( $this->post->$key ); + } + + /** + * Update the client's post. + * + * @param array $params Parameters to update. + * @return bool|WP_Error True on success, error object otherwise. + */ + public function update( $params ) { + $data = array(); + if ( isset( $params['name'] ) ) { + $data['post_title'] = $params['name']; + } + if ( isset( $params['description'] ) ) { + $data['post_content'] = $params['description']; + } + + // Are we updating the post itself? + if ( ! empty( $data ) ) { + $data['ID'] = $this->post->ID; + + $result = wp_update_post( wp_slash( $data ), true ); + if ( is_wp_error( $result ) ) { + return $result; + } + + // Reload the post property + $this->post = get_post( $this->post->ID ); + } + + // Are we updating any meta? + if ( ! empty( $params['meta'] ) ) { + $meta = $params['meta']; + + foreach ( $meta as $key => $value ) { + $existing = get_post_meta( $this->post->ID, $key, true ); + if ( $existing === $value ) { + continue; + } + + $did_update = update_post_meta( $this->post->ID, $key, wp_slash( $value ) ); + if ( ! $did_update ) { + return new WP_Error( + 'rest_client_update_meta_failed', + __( 'Could not update client metadata.', 'rest_oauth' ) + ); + } + } + } + + return true; + } + + /** + * Delete a client. + * + * @param string $type Client type. + * @param int $id Client post ID. + * @return bool True if delete, false otherwise. + */ + public function delete() { + return (bool) wp_delete_post( $this->post->ID, true ); + } + + /** + * Get a client by ID. + * + * @param int $id Client post ID. + * @return self|WP_Error + */ + public static function get( $id ) { + $post = get_post( $id ); + if ( empty( $id ) || empty( $post ) || $post->post_type !== 'json_consumer' ) { + return new WP_Error( 'rest_oauth1_invalid_id', __( 'Client ID is not valid.', 'rest_oauth1' ), array( 'status' => 404 ) ); + } + + $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); + return new $class( $post ); + } + + /** + * Get a client by key. + * + * @param string $type Client type. + * @param string $key Client key. + * @return WP_Post|WP_Error + */ + public static function get_by_key( $key ) { + $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); + $type = call_user_func( array( $class, 'get_type' ) ); + + $query = new WP_Query(); + $consumers = $query->query( array( + 'post_type' => 'json_consumer', + 'post_status' => 'any', + 'meta_query' => array( + array( + 'key' => 'key', + 'value' => $key, + ), + array( + 'key' => 'type', + 'value' => $type, + ), + ), + ) ); + + if ( empty( $consumers ) || empty( $consumers[0] ) ) { + return new WP_Error( 'json_consumer_notfound', __( 'Consumer Key is invalid' ), array( 'status' => 401 ) ); + } + + return $consumers[0]; + } + + /** + * Create a new client. + * + * @param string $type Client type. + * @param array $params { + * @type string $name Client name + * @type string $description Client description + * @type array $meta Metadata for the client (map of key => value) + * } + * @return WP_Post|WP_Error + */ + public static function create( $params ) { + $default = array( + 'name' => '', + 'description' => '', + 'meta' => array(), + ); + $params = wp_parse_args( $params, $default ); + + $data = array(); + $data['post_title'] = $params['name']; + $data['post_content'] = $params['description']; + $data['post_type'] = 'json_consumer'; + + $ID = wp_insert_post( $data ); + if ( is_wp_error( $ID ) ) { + return $ID; + } + + $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); + $meta = $params['meta']; + $meta['type'] = call_user_func( array( $class, 'get_type' ) ); + + // Allow types to add their own meta too + $meta = $class::add_extra_meta( $meta, $params ); + + /** + * Add extra meta to the consumer on creation. + * + * @param array $meta Metadata map of key => value + * @param int $ID Post ID we created. + * @param array $params Parameters passed to create. + */ + $meta = apply_filters( 'json_consumer_meta', $meta, $ID, $params ); + + foreach ( $meta as $key => $value ) { + update_post_meta( $ID, $key, $value ); + } + + $post = get_post( $ID ); + return new $class( $post ); + } + + /** + * Add extra meta to a post. + * + * If you'd like to add extra meta on client creation, add it here. This + * works the same as a filter; make sure you return the original array! + * + * @param array $meta Metadata for the post. + * @param array $params Parameters used to create the post. + * @return array Metadata to actually save. + */ + protected static function add_extra_meta( $meta, $params ) { + return $meta; + } + + /** + * Shim for get_called_class() for PHP 5.2 + * + * @return string Class name. + */ + protected static function get_called_class() { + // PHP 5.2 only + $backtrace = debug_backtrace(); + // [0] WP_REST_Client::get_called_class() + // [1] WP_REST_Client::function() + if ( 'call_user_func' === $backtrace[2]['function'] ) { + return $backtrace[2]['args'][0][0]; + } + return $backtrace[2]['class']; + } +} diff --git a/lib/class-wp-rest-oauth1-admin.php b/lib/class-wp-rest-oauth1-admin.php new file mode 100644 index 0000000..77e6952 --- /dev/null +++ b/lib/class-wp-rest-oauth1-admin.php @@ -0,0 +1,438 @@ + value, or wp_parse_args string. + * @return string Requested URL. + */ + protected static function get_url( $params = array() ) { + $url = admin_url( 'users.php' ); + $params = array( 'page' => self::BASE_SLUG ) + wp_parse_args( $params ); + return add_query_arg( urlencode_deep( $params ), $url ); + } + + /** + * Get the current page action. + * + * @return string One of 'add', 'edit', 'delete', or '' for default (list) + */ + protected static function current_action() { + return isset( $_GET['action'] ) ? $_GET['action'] : ''; + } + + /** + * Load data for our page. + */ + public static function load() { + switch ( self::current_action() ) { + case 'add': + case 'edit': + return self::render_edit_page(); + + case 'delete': + return self::handle_delete(); + + case 'regenerate': + return self::handle_regenerate(); + + default: + global $wp_list_table; + + $wp_list_table = new WP_REST_OAuth1_ListTable(); + + $wp_list_table->prepare_items(); + + return; + } + + } + + public static function dispatch() { + switch ( self::current_action() ) { + case 'add': + case 'edit': + case 'delete': + return; + + default: + return self::render(); + } + } + + /** + * Render the list page. + */ + public static function render() { + global $wp_list_table; + + ?> +
+

+ + + +

+

' . esc_html__( 'Deleted application.', 'rest_oauth1' ) . '

'; + } + ?> + + views(); ?> + + + + search_box( __( 'Search Applications', 'rest_oauth1' ), 'rest_oauth1' ); ?> + + display(); ?> + + + +
+ + + ID ); + } + + // Check that the parameters are correct first + $params = self::validate_parameters( wp_unslash( $_POST ) ); + if ( is_wp_error( $params ) ) { + $messages[] = $params->get_error_message(); + return $messages; + } + + if ( empty( $consumer ) ) { + $authenticator = new WP_REST_OAuth1(); + + // Create the consumer + $data = array( + 'name' => $params['name'], + 'description' => $params['description'], + 'meta' => array( + 'callback' => $params['callback'], + ), + ); + $consumer = $result = WP_REST_OAuth1_Client::create( $data ); + } + else { + // Update the existing consumer post + $data = array( + 'name' => $params['name'], + 'description' => $params['description'], + 'meta' => array( + 'callback' => $params['callback'], + ), + ); + $result = $consumer->update( $data ); + } + + if ( is_wp_error( $result ) ) { + $messages[] = $result->get_error_message(); + + return $messages; + } + + // Success, redirect to alias page + $location = self::get_url( + array( + 'action' => 'edit', + 'id' => $consumer->ID, + 'did_action' => $did_action, + ) + ); + wp_safe_redirect( $location ); + exit; + } + + /** + * Output alias editing page + */ + public static function render_edit_page() { + if ( ! current_user_can( 'edit_users' ) ) { + wp_die( __( 'You do not have permission to access this page.' ) ); + } + + // Are we editing? + $consumer = null; + $form_action = self::get_url('action=add'); + if ( ! empty( $_REQUEST['id'] ) ) { + $id = absint( $_REQUEST['id'] ); + $consumer = WP_REST_OAuth1_Client::get( $id ); + if ( is_wp_error( $consumer ) || empty( $consumer ) ) { + wp_die( __( 'Invalid consumer ID.' ) ); + } + + $form_action = self::get_url( array( 'action' => 'edit', 'id' => $id ) ); + $regenerate_action = self::get_url( array( 'action' => 'regenerate', 'id' => $id ) ); + } + + // Handle form submission + $messages = array(); + if ( ! empty( $_POST['submit'] ) ) { + $messages = self::handle_edit_submit( $consumer ); + } + if ( ! empty( $_GET['did_action'] ) ) { + switch ( $_GET['did_action'] ) { + case 'edit': + $messages[] = __( 'Updated application.', 'rest_oauth1' ); + break; + + case 'regenerate': + $messages[] = __( 'Regenerated secret.', 'rest_oauth1' ); + break; + + default: + $messages[] = __( 'Successfully created application.', 'rest_oauth1' ); + break; + } + } + + $data = array(); + + if ( empty( $consumer ) || ! empty( $_POST['_wpnonce'] ) ) { + foreach ( array( 'name', 'description', 'callback' ) as $key ) { + $data[ $key ] = empty( $_POST[ $key ] ) ? '' : wp_unslash( $_POST[ $key ] ); + } + } + else { + $data['name'] = $consumer->post_title; + $data['description'] = $consumer->post_content; + $data['callback'] = $consumer->callback; + } + + // Header time! + global $title, $parent_file, $submenu_file; + $title = $consumer ? __( 'Edit Application', 'rest_oauth1' ) : __( 'Add Application', 'rest_oauth1' ); + $parent_file = 'users.php'; + $submenu_file = self::BASE_SLUG; + + include( ABSPATH . 'wp-admin/admin-header.php' ); + ?> + +
+

+ +

' . esc_html( $msg ) . '

'; + } + ?> + + +
- +
- + + get_consumer( $row['consumer'] ); + $application = get_post($row['consumer']); ?> -
post_title ) ?> +
-

+

+ + + + + + + + + + + + +
+ + + +

+
+ + + +
+ + + +

+
+ + ID ) . '" />'; + wp_nonce_field( 'rest-oauth1-edit-' . $consumer->ID ); + submit_button( __( 'Save Consumer' ) ); + } + + ?> + + + +
+

+ + + + + + + + + + +
+ + + key ) ?> +
+ + + secret ) ?> +
+ + ID ); + submit_button( __( 'Regenerate Secret', 'rest_oauth1' ), 'delete' ); + ?> +
+ + + + ' . __( 'Cheatin’ uh?' ) . '' . + '

' . __( 'You are not allowed to delete this application.' ) . '

', + 403 + ); + } + + $client = WP_REST_OAuth1_Client::get( $id ); + if ( is_wp_error( $client ) ) { + wp_die( $client ); + return; + } + + if ( ! $client->delete() ) { + $message = 'Invalid consumer ID'; + wp_die( $message ); + return; + } + + wp_redirect( self::get_url( 'deleted=1' ) ); + exit; + } + + public static function handle_regenerate() { + if ( empty( $_GET['id'] ) ) { + return; + } + + $id = $_GET['id']; + check_admin_referer( 'rest-oauth1-regenerate:' . $id ); + + if ( ! current_user_can( 'edit_post', $id ) ) { + wp_die( + '

' . __( 'Cheatin’ uh?' ) . '

' . + '

' . __( 'You are not allowed to edit this application.' ) . '

', + 403 + ); + } + + $client = WP_REST_OAuth1_Client::get( $id ); + $client->regenerate_secret(); + + wp_redirect( self::get_url( array( 'action' => 'edit', 'id' => $id, 'did_action' => 'regenerate' ) ) ); + exit; + } +} diff --git a/lib/class-wp-json-authentication-oauth1-cli.php b/lib/class-wp-rest-oauth1-cli.php similarity index 67% rename from lib/class-wp-json-authentication-oauth1-cli.php rename to lib/class-wp-rest-oauth1-cli.php index f645fda..bbe2e10 100644 --- a/lib/class-wp-json-authentication-oauth1-cli.php +++ b/lib/class-wp-rest-oauth1-cli.php @@ -1,6 +1,6 @@ add_consumer( $args ); + $consumer = WP_REST_OAuth1_Client::create( $args ); WP_CLI::line( sprintf( 'ID: %d', $consumer->ID ) ); WP_CLI::line( sprintf( 'Key: %s', $consumer->key ) ); WP_CLI::line( sprintf( 'Secret: %s', $consumer->secret ) ); diff --git a/lib/class-wp-rest-oauth1-client.php b/lib/class-wp-rest-oauth1-client.php new file mode 100644 index 0000000..8c562b9 --- /dev/null +++ b/lib/class-wp-rest-oauth1-client.php @@ -0,0 +1,49 @@ + array( + 'secret' => wp_generate_password( self::CONSUMER_SECRET_LENGTH, false ), + ), + ); + + return $this->update( $params ); + } + + /** + * Get the client type. + * + * @return string + */ + protected static function get_type() { + return 'oauth1'; + } + + /** + * Add extra meta to a post. + * + * Adds the key and secret for a client to the meta on creation. Only adds + * them if they're not set, allowing them to be overridden for consumers + * with a pre-existing pair (such as via an import). + * + * @param array $meta Metadata for the post. + * @param array $params Parameters used to create the post. + * @return array Metadata to actually save. + */ + protected static function add_extra_meta( $meta, $params ) { + if ( empty( $meta['key'] ) && empty( $meta['secret'] ) ) { + $meta['key'] = wp_generate_password( self::CONSUMER_KEY_LENGTH, false ); + $meta['secret'] = wp_generate_password( self::CONSUMER_SECRET_LENGTH, false ); + } + return parent::add_extra_meta( $meta, $params ); + } +} diff --git a/lib/class-wp-json-authentication-oauth1-listtable.php b/lib/class-wp-rest-oauth1-listtable.php similarity index 56% rename from lib/class-wp-json-authentication-oauth1-listtable.php rename to lib/class-wp-rest-oauth1-listtable.php index 27da678..7e94704 100644 --- a/lib/class-wp-json-authentication-oauth1-listtable.php +++ b/lib/class-wp-rest-oauth1-listtable.php @@ -1,6 +1,6 @@ get_pagenum(); @@ -42,27 +42,42 @@ public function get_columns() { public function column_cb( $item ) { ?> - - + + + + ID ); if ( empty( $title ) ) { - $title = '' . __( 'Untitled' ) . ''; + $title = '' . esc_html__( 'Untitled', 'rest_oauth1' ) . ''; } $edit_link = add_query_arg( array( - 'action' => 'json-oauth-edit', + 'page' => 'rest-oauth1-apps', + 'action' => 'edit', + 'id' => $item->ID, + ), + admin_url( 'users.php' ) + ); + $delete_link = add_query_arg( + array( + 'page' => 'rest-oauth1-apps', + 'action' => 'delete', 'id' => $item->ID, ), - admin_url( 'admin.php' ) + admin_url( 'users.php' ) ); + $delete_link = wp_nonce_url( $delete_link, 'rest-oauth1-delete:' . $item->ID ); $actions = array( - 'edit' => sprintf( '%s', $edit_link, __( 'Edit' ) ), + 'edit' => sprintf( '%s', $edit_link, esc_html__( 'Edit', 'rest_oauth1' ) ), + 'delete' => sprintf( '%s', $delete_link, esc_html__( 'Delete', 'rest_oauth1' ) ), ); $action_html = $this->row_actions( $actions ); diff --git a/lib/class-wp-json-authentication-oauth1-authorize.php b/lib/class-wp-rest-oauth1-ui.php similarity index 96% rename from lib/class-wp-json-authentication-oauth1-authorize.php rename to lib/class-wp-rest-oauth1-ui.php index 567be24..ecfa82b 100644 --- a/lib/class-wp-json-authentication-oauth1-authorize.php +++ b/lib/class-wp-rest-oauth1-ui.php @@ -8,7 +8,7 @@ * @subpackage JSON API */ -class WP_JSON_Authentication_OAuth1_Authorize { +class WP_REST_OAuth1_UI { /** * Request token for the current authorization request * @@ -68,7 +68,7 @@ public function render_page() { $scope = wp_unslash( $_REQUEST['wp_scope'] ); } - $authenticator = new WP_JSON_Authentication_OAuth1(); + $authenticator = new WP_REST_OAuth1(); $errors = array(); $this->token = $authenticator->get_request_token( $token_key ); if ( is_wp_error( $this->token ) ) { @@ -152,8 +152,8 @@ public function handle_callback_redirect( $verifier ) { $callback = $this->token['callback']; // Ensure the URL is safe to access - $callback = wp_http_validate_url( $callback ); - if ( empty( $callback ) ) { + $authenticator = new WP_REST_OAuth1(); + if ( ! $authenticator->check_callback( $callback, $this->token['consumer'] ) ) { return new WP_Error( 'json_oauth1_invalid_callback', __( 'The callback URL is invalid' ), array( 'status' => 400 ) ); } diff --git a/lib/class-wp-json-authentication-oauth1.php b/lib/class-wp-rest-oauth1.php similarity index 84% rename from lib/class-wp-json-authentication-oauth1.php rename to lib/class-wp-rest-oauth1.php index 913a3f5..4632788 100644 --- a/lib/class-wp-json-authentication-oauth1.php +++ b/lib/class-wp-rest-oauth1.php @@ -6,9 +6,7 @@ * @subpackage JSON API */ -class WP_JSON_Authentication_OAuth1 extends WP_JSON_Authentication { - const CONSUMER_KEY_LENGTH = 12; - const CONSUMER_SECRET_LENGTH = 48; +class WP_REST_OAuth1 { const TOKEN_KEY_LENGTH = 24; const TOKEN_SECRET_LENGTH = 48; const VERIFIER_LENGTH = 24; @@ -280,29 +278,6 @@ public function dispatch( $route ) { } } - /** - * Add a new consumer - * - * Ensures that the consumer has an associated key/secret pair, which can be - * overridden for consumers with a pre-existing pair (such as via an import) - * - * @param array $params Consumer parameters - * @return WP_Post Consumer data - */ - public function add_consumer( $params ) { - $meta = array( - 'key' => wp_generate_password( self::CONSUMER_KEY_LENGTH, false ), - 'secret' => wp_generate_password( self::CONSUMER_SECRET_LENGTH, false ), - ); - - if ( empty( $params['meta'] ) ) { - $params['meta'] = array(); - } - $params['meta'] = array_merge( $params['meta'], $meta ); - - return parent::add_consumer( $params ); - } - /** * Check a token against the database * @@ -311,7 +286,10 @@ public function add_consumer( $params ) { * @return array Array of consumer object, user object */ public function check_token( $token, $consumer_key ) { - $consumer = $this->get_consumer( $consumer_key ); + $this->should_attempt = false; + $consumer = WP_REST_OAuth1_Client::get_by_key( $consumer_key ); + $this->should_attempt = true; + if ( is_wp_error( $consumer ) ) { return $consumer; } @@ -352,7 +330,7 @@ public function get_request_token( $key ) { * @return array|WP_Error Array of token data on success, error otherwise */ public function generate_request_token( $params ) { - $consumer = $this->get_consumer( $params['oauth_consumer_key'] ); + $consumer = WP_REST_OAuth1_Client::get_by_key( $params['oauth_consumer_key'] ); if ( is_wp_error( $consumer ) ) { return $consumer; } @@ -377,6 +355,12 @@ public function generate_request_token( $params ) { ); $data = apply_filters( 'json_oauth1_request_token_data', $data ); add_option( 'oauth1_request_' . $key, $data, null, 'no' ); + if ( ! empty( $params['oauth_callback'] ) ) { + $error = $this->set_request_token_callback( $key, $params['oauth_callback'] ); + if ( $error ) { + return $error; + } + } $data = array( 'oauth_token' => self::urlencode_rfc3986($key), @@ -392,7 +376,8 @@ public function set_request_token_callback( $key, $callback ) { return $token; } - if ( esc_url_raw( $callback ) !== $callback ) { + $consumer = $token['consumer']; + if ( ! $this->check_callback( $callback, $consumer ) ) { return new WP_Error( 'json_oauth1_invalid_callback', __( 'Callback URL is invalid' ) ); } @@ -401,6 +386,103 @@ public function set_request_token_callback( $key, $callback ) { return $token['verifier']; } + /** + * Validate a callback URL. + * + * Based on {@see wp_http_validate_url}, but less restrictive around ports + * and hosts. In particular, it allows any scheme, host or port rather than + * just HTTP with standard ports. + * + * @param string $url URL for the callback. + * @return bool True for a valid callback URL, false otherwise. + */ + protected function validate_callback( $url ) { + if ( strpos( $url, ':' ) === false ) { + return false; + } + + $parsed_url = wp_parse_url( $url ); + if ( ! $parsed_url || empty( $parsed_url['host'] ) ) + return false; + + if ( isset( $parsed_url['user'] ) || isset( $parsed_url['pass'] ) ) + return false; + + if ( false !== strpbrk( $parsed_url['host'], ':#?[]' ) ) + return false; + + return true; + } + + /** + * Check whether a callback is valid for a given consumer. + * + * @param string $url Supplied callback. + * @param int|WP_Post $consumer_id Consumer post ID or object. + * @return bool True if valid, false otherwise. + */ + public function check_callback( $url, $consumer_id ) { + $consumer = get_post( $consumer_id ); + if ( empty( $consumer ) || $consumer->post_type !== 'json_consumer' || $consumer->type !== $this->type ) { + return false; + } + + $registered = $consumer->callback; + if ( empty( $registered ) ) { + return false; + } + + // Out-of-band isn't a URL, but is still valid + if ( $registered === 'oob' || $url === 'oob' ) { + // Ensure both the registered URL and requested are 'oob' + return ( $registered === $url ); + } + + // Validate the supplied URL + if ( ! $this->validate_callback( $url ) ) { + return false; + } + + $registered = wp_parse_url( $registered ); + $supplied = wp_parse_url( $url ); + + // Check all components except query and fragment + $parts = array( 'scheme', 'host', 'port', 'user', 'pass', 'path' ); + $valid = true; + foreach ( $parts as $part ) { + if ( isset( $registered[ $part ] ) !== isset( $supplied[ $part ] ) ) { + $valid = false; + break; + } + + if ( ! isset( $registered[ $part ] ) ) { + continue; + } + + if ( $registered[ $part ] !== $supplied[ $part ] ) { + $valid = false; + break; + } + } + + /** + * Filter whether a callback is counted as valid. + * + * By default, the URLs must match scheme, host, port, user, pass, and + * path. Query and fragment segments are allowed to be different. + * + * To change this behaviour, filter this value. Note that consumers must + * have a callback registered, even if you relax this restruction. It is + * highly recommended not to change this behaviour, as clients will + * expect the same behaviour across all WP sites. + * + * @param boolean $valid True if the callback URL is valid, false otherwise. + * @param string $url Supplied callback URL. + * @param WP_Post $consumer Consumer post; stored callback saved as `consumer` meta value. + */ + return apply_filters( 'rest_oauth.check_callback', $valid, $url, $consumer ); + } + /** * Authorize a request token * @@ -479,7 +561,10 @@ public function generate_access_token( $oauth_consumer_key, $oauth_token, $oauth return new WP_Error( 'json_oauth1_invalid_verifier', __( 'OAuth verifier does not match' ), array( 'status' => 400 ) ); } - $consumer = $this->get_consumer( $oauth_consumer_key ); + $this->should_attempt = false; + $consumer = WP_REST_OAuth1_Client::get_by_key( $oauth_consumer_key ); + $this->should_attempt = true; + if ( is_wp_error( $consumer ) ) { return $consumer; } diff --git a/oauth-server.php b/oauth-server.php index f5938b9..98f3431 100644 --- a/oauth-server.php +++ b/oauth-server.php @@ -1,39 +1,55 @@ add_query_var('json_oauth_route'); + $wp->add_query_var('rest_oauth1'); } -add_action( 'init', 'json_oauth_server_init' ); +add_action( 'init', 'rest_oauth1_init' ); -function json_oauth_server_register_rewrites() { - add_rewrite_rule( '^oauth1/authorize/?$','index.php?json_oauth_route=authorize','top' ); - add_rewrite_rule( '^oauth1/request/?$','index.php?json_oauth_route=request','top' ); - add_rewrite_rule( '^oauth1/access/?$','index.php?json_oauth_route=access','top' ); +function rest_oauth1_register_rewrites() { + add_rewrite_rule( '^oauth1/authorize/?$','index.php?rest_oauth1=authorize','top' ); + add_rewrite_rule( '^oauth1/request/?$','index.php?rest_oauth1=request','top' ); + add_rewrite_rule( '^oauth1/access/?$','index.php?rest_oauth1=access','top' ); } -function json_oauth_server_setup_authentication() { +function rest_oauth1_setup_authentication() { register_post_type( 'json_consumer', array( 'labels' => array( 'name' => __( 'Consumer' ), @@ -46,7 +62,7 @@ function json_oauth_server_setup_authentication() { 'query_var' => false, ) ); } -add_action( 'init', 'json_oauth_server_setup_authentication' ); +add_action( 'init', 'rest_oauth1_setup_authentication' ); /** * Register the authorization page @@ -54,14 +70,14 @@ function json_oauth_server_setup_authentication() { * Alas, login_init is too late to register pages, as the action is already * sanitized before this. */ -function json_oauth_load() { +function rest_oauth1_load() { global $wp_json_authentication_oauth1; - $wp_json_authentication_oauth1 = new WP_JSON_Authentication_OAuth1(); + $wp_json_authentication_oauth1 = new WP_REST_OAuth1(); add_filter( 'determine_current_user', array( $wp_json_authentication_oauth1, 'authenticate' ) ); - add_filter( 'json_authentication_errors', array( $wp_json_authentication_oauth1, 'get_authentication_errors' ) ); + add_filter( 'rest_authentication_errors', array( $wp_json_authentication_oauth1, 'get_authentication_errors' ) ); } -add_action( 'init', 'json_oauth_load' ); +add_action( 'init', 'rest_oauth1_load' ); /** * Force reauthentication after we've registered our handler @@ -69,7 +85,7 @@ function json_oauth_load() { * We could have checked authentication before OAuth was loaded. If so, let's * try and reauthenticate now that OAuth is loaded. */ -function json_oauth_force_reauthentication() { +function rest_oauth1_force_reauthentication() { if ( is_user_logged_in() ) { // Another handler has already worked successfully, no need to // reauthenticate. @@ -82,17 +98,17 @@ function json_oauth_force_reauthentication() { $current_user = null; get_currentuserinfo(); } -add_action( 'init', 'json_oauth_force_reauthentication', 100 ); +add_action( 'init', 'rest_oauth1_force_reauthentication', 100 ); /** * Load the JSON API */ -function json_oauth_server_loaded() { - if ( empty( $GLOBALS['wp']->query_vars['json_oauth_route'] ) ) +function rest_oauth1_loaded() { + if ( empty( $GLOBALS['wp']->query_vars['rest_oauth1'] ) ) return; - $authenticator = new WP_JSON_Authentication_OAuth1(); - $response = $authenticator->dispatch( $GLOBALS['wp']->query_vars['json_oauth_route'] ); + $authenticator = new WP_REST_OAuth1(); + $response = $authenticator->dispatch( $GLOBALS['wp']->query_vars['rest_oauth1'] ); if ( is_wp_error( $response ) ) { $error_data = $response->get_error_data(); @@ -116,36 +132,15 @@ function json_oauth_server_loaded() { // Finish off our request die(); } -add_action( 'template_redirect', 'json_oauth_server_loaded', -100 ); - -/** - * Register v1 API routes - * - * @param array $data Index data - * @return array Filtered data - */ -function json_oauth_api_routes( $data ) { - if (empty($data['authentication'])) { - $data['authentication'] = array(); - } - - $data['authentication']['oauth1'] = array( - 'request' => home_url( 'oauth1/request' ), - 'authorize' => home_url( 'oauth1/authorize' ), - 'access' => home_url( 'oauth1/access' ), - 'version' => '0.1', - ); - return $data; -} -add_filter( 'json_index', 'json_oauth_api_routes' ); +add_action( 'template_redirect', 'rest_oauth1_loaded', -100 ); /** * Register v2 API routes * - * @param object $response_object WP_REST_Response Object + * @param object $response_object WP_REST_Response Object * @return object Filtered WP_REST_Response object */ -function json_oauth_api_routes_v2( $response_object ) { +function rest_oauth1_register_routes( $response_object ) { if ( empty( $response_object->data['authentication'] ) ) { $response_object->data['authentication'] = array(); } @@ -158,7 +153,7 @@ function json_oauth_api_routes_v2( $response_object ) { ); return $response_object; } -add_filter( 'rest_index', 'json_oauth_api_routes_v2' ); +add_filter( 'rest_index', 'rest_oauth1_register_routes' ); /** * Register the authorization page @@ -166,16 +161,16 @@ function json_oauth_api_routes_v2( $response_object ) { * Alas, login_init is too late to register pages, as the action is already * sanitized before this. */ -function json_oauth_load_authorize_page() { - $authorizer = new WP_JSON_Authentication_OAuth1_Authorize(); +function rest_oauth1_load_authorize_page() { + $authorizer = new WP_REST_OAuth1_UI(); $authorizer->register_hooks(); } -add_action( 'init', 'json_oauth_load_authorize_page' ); +add_action( 'init', 'rest_oauth1_load_authorize_page' ); /** * Register routes and flush the rewrite rules on activation. */ -function json_oauth_server_activation( $network_wide ) { +function rest_oauth1_activation( $network_wide ) { if ( function_exists( 'is_multisite' ) && is_multisite() && $network_wide ) { $mu_blogs = wp_get_sites(); @@ -183,7 +178,7 @@ function json_oauth_server_activation( $network_wide ) { foreach ( $mu_blogs as $mu_blog ) { switch_to_blog( $mu_blog['blog_id'] ); - json_oauth_server_register_rewrites(); + rest_oauth1_register_rewrites(); flush_rewrite_rules(); } @@ -191,16 +186,16 @@ function json_oauth_server_activation( $network_wide ) { } else { - json_oauth_server_register_rewrites(); + rest_oauth1_register_rewrites(); flush_rewrite_rules(); } } -register_activation_hook( __FILE__, 'json_oauth_server_activation' ); +register_activation_hook( __FILE__, 'rest_oauth1_activation' ); /** * Flush the rewrite rules on deactivation */ -function json_oauth_server_deactivation( $network_wide ) { +function rest_oauth1_deactivation( $network_wide ) { if ( function_exists( 'is_multisite' ) && is_multisite() && $network_wide ) { $mu_blogs = wp_get_sites(); @@ -218,4 +213,4 @@ function json_oauth_server_deactivation( $network_wide ) { flush_rewrite_rules(); } } -register_deactivation_hook( __FILE__, 'json_oauth_server_deactivation' ); +register_deactivation_hook( __FILE__, 'rest_oauth1_deactivation' );