diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..7a364dd --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,28 @@ + + + Generally-applicable sniffs for WordPress plugins. + + + . + /vendor/ + /node_modules/ + + + + + + + + + + + + + + + + + + + + diff --git a/.travis.yml b/.travis.yml index 980b383..1f65be6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,58 @@ +sudo: false +dist: trusty + language: php -php: - - '7.1' -install: - - composer install - - bash tests/install-tests.sh wordpress_test root '' 127.0.0.1 latest + +notifications: + email: + on_success: never + on_failure: change + +branches: + only: + - master + +cache: + directories: + - $HOME/.composer/cache + +matrix: + include: + - php: 7.3 + env: WP_VERSION=latest + - php: 7.2 + env: WP_VERSION=latest + - php: 7.1 + env: WP_VERSION=latest + - php: 7.0 + env: WP_VERSION=latest + - php: 7.0 + env: WP_VERSION=trunk + - php: 7.0 + env: WP_TRAVISCI=phpcs + dist: precise + +before_script: + - export PATH="$HOME/.composer/vendor/bin:$PATH" + - composer install --ignore-platform-reqs --optimize-autoloader --no-interaction --prefer-dist + - | + if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then + phpenv config-rm xdebug.ini + else + echo "xdebug.ini does not exist" + fi + - | + if [[ ! -z "$WP_VERSION" ]] ; then + bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + fi + script: - - vendor/bin/phpcs --standard=phpcs.ruleset.xml . - - phpunit + - | + if [[ ! -z "$WP_VERSION" ]] ; then + vendor/bin/phpunit + WP_MULTISITE=1 vendor/bin/phpunit + fi + - | + if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then + vendor/bin/phpcs + fi diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100755 index 0000000..5ceac4b --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +TMPDIR=${TMPDIR-/tmp} +TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") +WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then + WP_BRANCH=${WP_VERSION%\-*} + WP_TESTS_TAG="branches/$WP_BRANCH" + +elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then + WP_TESTS_TAG="branches/$WP_VERSION" +elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + WP_TESTS_TAG="tags/${WP_VERSION%??}" + else + WP_TESTS_TAG="tags/$WP_VERSION" + fi +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p $TMPDIR/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip + unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ + mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then + # https serves multiple offers, whereas http serves single. + download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + LATEST_VERSION=${WP_VERSION%??} + else + # otherwise, scan the releases and get the most up to date minor version of the major release + local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` + LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) + fi + if [[ -z "$LATEST_VERSION" ]]; then + local ARCHIVE_NAME="wordpress-$WP_VERSION" + else + local ARCHIVE_NAME="wordpress-$LATEST_VERSION" + fi + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i.bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + fi + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db diff --git a/composer.json b/composer.json index 35fc0bd..210c30d 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,19 @@ "homepage": "http://wp-api.org/" } ], + "require": { + "composer/installers": "~1.0", + "php": "^5.6.0||^7.0" + }, "require-dev": { - "humanmade/coding-standards": "dev-master" + "squizlabs/php_codesniffer": "^3.3.1", + "wp-coding-standards/wpcs": "^2.1.1", + "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", + "phpcompatibility/phpcompatibility-wp": "^2.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0" + }, + "scripts": { + "post-install-cmd": "\"vendor/bin/phpcs\" --config-set installed_paths vendor/wp-coding-standards/wpcs", + "post-update-cmd" : "\"vendor/bin/phpcs\" --config-set installed_paths vendor/wp-coding-standards/wpcs" } } diff --git a/inc/admin/class-listtable.php b/inc/admin/class-listtable.php index b5a2cf4..83331b9 100644 --- a/inc/admin/class-listtable.php +++ b/inc/admin/class-listtable.php @@ -1,4 +1,9 @@ ID ); ?>"> - post_type ); - if ( current_user_can( $post_type_object->cap->publish_posts ) && $item->post_status !== 'publish' ) { + if ( current_user_can( $post_type_object->cap->publish_posts ) && 'publish' !== $item->post_status ) { $publish_link = add_query_arg( [ 'page' => 'rest-oauth2-apps', diff --git a/inc/admin/namespace.php b/inc/admin/namespace.php index 963ac95..aa79fb0 100644 --- a/inc/admin/namespace.php +++ b/inc/admin/namespace.php @@ -1,4 +1,9 @@ prepare_items(); return; } - } +/** + * + */ function dispatch() { switch ( get_page_action() ) { case 'add': @@ -112,16 +119,16 @@ function render() { if ( current_user_can( 'create_users' ) ) : ?> -

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

'; - } elseif ( ! empty( $_GET['approved'] ) ) { // WPCS: CSRF OK + } elseif ( ! empty( $_GET['approved'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended echo '

' . esc_html__( 'Approved application.', 'oauth2' ) . '

'; } ?> @@ -130,7 +137,7 @@ class="add-new-h2"> - search_box( __( 'Search Applications', 'oauth2' ), 'oauth2' ); ?> + search_box( esc_html__( 'Search Applications', 'oauth2' ), 'oauth2' ); ?> display(); ?> @@ -152,22 +159,22 @@ function validate_parameters( $params ) { $valid = []; if ( empty( $params['name'] ) ) { - return new WP_Error( 'rest_oauth2_missing_name', __( 'Client name is required', 'oauth2' ) ); + return new WP_Error( 'rest_oauth2_missing_name', esc_html__( 'Client name is required', 'oauth2' ) ); } $valid['name'] = wp_kses_post( $params['name'] ); if ( empty( $params['description'] ) ) { - return new WP_Error( 'rest_oauth2_missing_description', __( 'Client description is required', 'oauth2' ) ); + return new WP_Error( 'rest_oauth2_missing_description', esc_html__( 'Client description is required', 'oauth2' ) ); } $valid['description'] = wp_kses_post( $params['description'] ); if ( empty( $params['type'] ) ) { - return new WP_Error( 'rest_oauth2_missing_type', __( 'Type is required.', 'oauth2' ) ); + return new WP_Error( 'rest_oauth2_missing_type', esc_html__( 'Type is required.', 'oauth2' ) ); } $valid['type'] = wp_kses_post( $params['type'] ); if ( empty( $params['callback'] ) ) { - return new WP_Error( 'rest_oauth2_missing_callback', __( 'Client callback is required and must be a valid URL.', 'oauth2' ) ); + return new WP_Error( 'rest_oauth2_missing_callback', esc_html__( 'Client callback is required and must be a valid URL.', 'oauth2' ) ); } if ( ! empty( $params['callback'] ) ) { $valid['callback'] = $params['callback']; @@ -252,7 +259,7 @@ function handle_edit_submit( Client $consumer = null ) { */ function render_edit_page() { if ( ! current_user_can( 'edit_users' ) ) { - wp_die( __( 'You do not have permission to access this page.', 'oauth2' ) ); + wp_die( esc_html__( 'You do not have permission to access this page.', 'oauth2' ) ); } // Are we editing? @@ -263,17 +270,21 @@ function render_edit_page() { $id = absint( $_REQUEST['id'] ); $consumer = Client::get_by_post_id( $id ); if ( is_wp_error( $consumer ) || empty( $consumer ) ) { - wp_die( __( 'Invalid client ID.', 'oauth2' ) ); + wp_die( esc_html__( 'Invalid client ID.', 'oauth2' ) ); } - $form_action = get_url( [ - 'action' => 'edit', - 'id' => $id, - ] ); - $regenerate_action = get_url( [ - 'action' => 'regenerate', - 'id' => $id, - ] ); + $form_action = get_url( + [ + 'action' => 'edit', + 'id' => $id, + ] + ); + $regenerate_action = get_url( + [ + 'action' => 'regenerate', + 'id' => $id, + ] + ); } // Handle form submission @@ -292,15 +303,15 @@ function render_edit_page() { if ( ! empty( $_GET['did_action'] ) ) { switch ( $_GET['did_action'] ) { case 'edit': - $messages[] = __( 'Updated application.', 'oauth2' ); + $messages[] = esc_html__( 'Updated application.', 'oauth2' ); break; case 'regenerate': - $messages[] = __( 'Regenerated secret.', 'oauth2' ); + $messages[] = esc_html__( 'Regenerated secret.', 'oauth2' ); break; default: - $messages[] = __( 'Successfully created application.', 'oauth2' ); + $messages[] = esc_html__( 'Successfully created application.', 'oauth2' ); break; } } @@ -324,15 +335,17 @@ function render_edit_page() { // Header time! global $title, $parent_file, $submenu_file; - $title = $consumer ? __( 'Edit Application', 'oauth2' ) : __( 'Add Application', 'oauth2' ); + // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited + $title = $consumer ? esc_html__( 'Edit Application', 'oauth2' ) : esc_html__( 'Add Application', 'oauth2' ); $parent_file = 'users.php'; $submenu_file = BASE_SLUG; + // phpcs:enable - include( ABSPATH . 'wp-admin/admin-header.php' ); + include ABSPATH . 'wp-admin/admin-header.php'; ?>
-

+

-
+
- + - -

+ +

- + - +
- +
    @@ -412,11 +425,11 @@ function render_edit_page() {
- + - -

+ +

@@ -425,42 +438,42 @@ function render_edit_page() { if ( empty( $consumer ) ) { wp_nonce_field( 'rest-oauth2-add' ); - submit_button( __( 'Create Client', 'oauth2' ) ); + submit_button( esc_html__( 'Create Client', 'oauth2' ) ); } else { echo ''; wp_nonce_field( 'rest-oauth2-edit-' . $consumer->get_post_id() ); - submit_button( __( 'Save Client', 'oauth2' ) ); + submit_button( esc_html__( 'Save Client', 'oauth2' ) ); } ?>
-
-

+ +

- + - get_id() ) ?> + get_id() ); ?>
- + - get_secret() ) ?> + get_secret() ); ?>
get_post_id() ); - submit_button( __( 'Regenerate Secret', 'oauth2' ), 'delete' ); + submit_button( esc_html__( 'Regenerate Secret', 'oauth2' ), 'delete' ); ?>
@@ -482,22 +495,21 @@ function handle_delete() { if ( ! current_user_can( 'delete_post', $id ) ) { wp_die( - '

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

' . - '

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

', + '

' . esc_html__( 'Cheatin’ uh?', 'oauth2' ) . '

' . + '

' . esc_html__( 'You are not allowed to delete this application.', 'oauth2' ) . '

', 403 ); } $client = Client::get_by_post_id( $id ); if ( is_wp_error( $client ) ) { - wp_die( $client ); + wp_die( $client ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped return; } if ( ! $client->delete() ) { - $message = 'Invalid client ID'; - wp_die( $message ); + wp_die( esc_html__( 'Invalid client ID' ) ); return; } @@ -519,20 +531,20 @@ function handle_approve() { if ( ! current_user_can( 'publish_post', $id ) ) { wp_die( - '

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

' . - '

' . __( 'You are not allowed to approve this application.', 'oauth2' ) . '

', + '

' . esc_html__( 'Cheatin’ uh?', 'oauth2' ) . '

' . + '

' . esc_html__( 'You are not allowed to approve this application.', 'oauth2' ) . '

', 403 ); } $client = Client::get_by_post_id( $id ); if ( is_wp_error( $client ) ) { - wp_die( $client ); + wp_die( $client ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } $did_approve = $client->approve(); if ( is_wp_error( $did_approve ) ) { - wp_die( $did_approve ); + wp_die( $did_approve ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } wp_safe_redirect( get_url( 'approved=1' ) ); @@ -552,8 +564,8 @@ function handle_regenerate() { if ( ! current_user_can( 'edit_post', $id ) ) { wp_die( - '

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

' . - '

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

', + '

' . esc_html__( 'Cheatin’ uh?', 'oauth2' ) . '

' . + '

' . esc_html__( 'You are not allowed to edit this application.', 'oauth2' ) . '

', 403 ); } @@ -561,13 +573,17 @@ function handle_regenerate() { $client = Client::get_by_post_id( $id ); $result = $client->regenerate_secret(); if ( is_wp_error( $result ) ) { - wp_die( $result->get_error_message() ); + wp_die( esc_html( $result->get_error_message() ) ); } - wp_safe_redirect( get_url( [ - 'action' => 'edit', - 'id' => $id, - 'did_action' => 'regenerate', - ] ) ); + wp_safe_redirect( + get_url( + [ + 'action' => 'edit', + 'id' => $id, + 'did_action' => 'regenerate', + ] + ) + ); exit; } diff --git a/inc/admin/profile/namespace.php b/inc/admin/profile/namespace.php index 61a2bbb..85d1a16 100644 --- a/inc/admin/profile/namespace.php +++ b/inc/admin/profile/namespace.php @@ -1,6 +1,9 @@ get_client(); - }); + $tokens = array_filter( + $tokens, + function ( Access_Token $token ) { + return (bool) $token->get_client(); + } + ); if ( ! IS_PROFILE_PAGE ) { $personal_url = PersonalTokens\get_page_url( [ 'user_id' => $user->ID ] ); @@ -40,7 +46,7 @@ function render_profile_section( WP_User $user ) { } ?> -

+

@@ -57,24 +63,29 @@ function render_profile_section( WP_User $user ) { ?> - - - + + +
- - - -
+ + + +
-

-

+

+

+ +

get_client(); @@ -97,9 +108,9 @@ function render_token_row( WP_User $user, Access_Token $token ) { /** * Filter details shown for an access token on the profile screen. * - * @param string[] $details List of HTML snippets to render in table. - * @param Access_Token $token Token being displayed. - * @param WP_User $user User whose profile is being rendered. + * @param string[] $details List of HTML snippets to render in table. + * @param Access_Token $token Token being displayed. + * @param WP_User $user User whose profile is being rendered. */ $details = apply_filters( 'oauth2.admin.profile.render_token_row.details', $details, $token, $user ); @@ -130,9 +141,9 @@ function render_token_row( WP_User $user, Access_Token $token ) { /** * Filter actions shown for an access token on the profile screen. * - * @param string[] $actions List of HTML snippets to render in table. - * @param Access_Token $token Token being displayed. - * @param WP_User $user User whose profile is being rendered. + * @param string[] $actions List of HTML snippets to render in table. + * @param Access_Token $token Token being displayed. + * @param WP_User $user User whose profile is being rendered. */ $actions = apply_filters( 'oauth2.admin.profile.render_token_row.actions', $actions, $token, $user ); @@ -147,11 +158,11 @@ function render_token_row( WP_User $user, Access_Token $token ) { ?> -

-

+

+

- +

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

'; + if ( ! empty( $_GET['oauth2_revoked'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + echo '

' . esc_html__( 'Token revoked.', 'oauth2' ) . '

'; } - if ( ! empty( $_GET['oauth2_revocation_failed'] ) ) { // WPCS: CSRF OK - echo '

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

'; + if ( ! empty( $_GET['oauth2_revocation_failed'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + echo '

' . esc_html__( 'Unable to revoke token.', 'oauth2' ) . '

'; } } @@ -184,7 +195,7 @@ function handle_revocation( $user_id ) { return; } - $data = wp_unslash( $_POST['oauth2_revoke'] ); // WPCS: CSRF OK + $data = sanitize_text_field( wp_unslash( $_POST['oauth2_revoke'] ) ); if ( strpos( $data, ':' ) === null ) { return; } diff --git a/inc/admin/profile/personaltokens/namespace.php b/inc/admin/profile/personaltokens/namespace.php index 2efdfcf..f500cee 100644 --- a/inc/admin/profile/personaltokens/namespace.php +++ b/inc/admin/profile/personaltokens/namespace.php @@ -1,4 +1,9 @@

' . esc_html( $error->get_error_message() ) . '

'; - } ); + add_action( + 'all_admin_notices', + function () use ( $error ) { + echo '

' . esc_html( $error->get_error_message() ) . '

'; + } + ); } } - $GLOBALS['title'] = __( 'Personal Access Tokens', 'oauth2' ); + $GLOBALS['title'] = __( 'Personal Access Tokens', 'oauth2' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited require ABSPATH . 'wp-admin/admin-header.php'; $tokens = Access_Token::get_for_user( $user ); - $tokens = array_filter( $tokens, function ( Access_Token $token ) { - $client = $token->get_client(); - return ! empty( $client ) && $client instanceof PersonalClient; - }); + $tokens = array_filter( + $tokens, + function ( Access_Token $token ) { + $client = $token->get_client(); + return ! empty( $client ) && $client instanceof PersonalClient; + } + ); ?>
-

+

-

+

@@ -115,17 +129,17 @@ class="regular-text" type="text" /> -

+

- +

- +

@@ -138,7 +152,14 @@ class="regular-text" * Handle action from a form. */ function handle_page_action( WP_User $user ) { - $action = $_POST['oauth2_action']; // WPCS: CSRF OK + if ( ! isset( $_POST['oauth2_action'] ) ) { + return new WP_Error( + 'rest_oauth2_invalid_action', + __( 'Invalid action.', 'oauth2' ) + ); + } + + $action = sanitize_text_field( wp_unslash( $_POST['oauth2_action'] ) ); switch ( $action ) { case 'create': @@ -174,18 +195,22 @@ function handle_create( WP_User $user, $name ) { render_create_success( $user, $token ); } +/** + * @param WP_User $user + * @param $token + */ function render_create_success( WP_User $user, $token ) { - $GLOBALS['title'] = __( 'Personal Access Tokens', 'oauth2' ); + $GLOBALS['title'] = __( 'Personal Access Tokens', 'oauth2' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited require ABSPATH . 'wp-admin/admin-header.php'; ?>
-

-

+

+

-
get_key() ) ?>
+
get_key() ); ?>
-

+

', ']]>', $content ); // Restore previous post. - $post = $current_post; + $post = $current_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited if ( $post ) { setup_postdata( $post ); } @@ -132,6 +138,7 @@ public function get_redirect_uris() { * just HTTP with standard ports. * * @param string $url URL for the callback. + * * @return bool True for a valid callback URL, false otherwise. */ public static function validate_callback( $url ) { @@ -158,10 +165,11 @@ public static function validate_callback( $url ) { /** * Check if a redirect URI is valid for the client. * - * @todo Implement this properly :) - * * @param string $uri Supplied redirect URI to check. + * * @return boolean True if the URI is valid, false otherwise. + * @todo Implement this properly :) + * */ public function check_redirect_uri( $uri ) { if ( ! $this->validate_callback( $uri ) ) { @@ -199,22 +207,16 @@ public function check_redirect_uri( $uri ) { } /** - * 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. + * Filter whether a callback is counted as valid. (deprecated). + * User rest_oauth_check_callback. * - * 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 string $registered_uri URI being checked. - * @param Client $client OAuth 2 client object. + * @param boolean $valid True if the callback URL is valid, false otherwise. + * @param string $url Supplied callback URL. + * @param string $registered_uri URI being checked. + * @param Client $client OAuth 2 client object. */ $valid = apply_filters( 'rest_oauth.check_callback', $valid, $uri, $registered_uri, $this ); + if ( $valid ) { // Stop checking, we have a match. return true; @@ -237,6 +239,7 @@ public function generate_authorization_code( WP_User $user ) { * Get data stored for an authorization code. * * @param string $code Authorization code to fetch. + * * @return Authorization_Code|WP_Error Data if available, error if invalid code. */ public function get_authorization_code( $code ) { @@ -259,7 +262,7 @@ public function regenerate_secret() { * Issue token for a user. * * @param \WP_User $user - * @param array $meta + * @param array $meta * * @return Access_Token */ @@ -271,6 +274,7 @@ public function issue_token( WP_User $user, $meta = [] ) { * Get a client by ID. * * @param string $id Client ID. + * * @return static|null Token if ID is found, null otherwise. */ public static function get_by_id( $id ) { @@ -295,6 +299,7 @@ public static function get_by_id( $id ) { * Get a client by post ID. * * @param int $id Client/post ID. + * * @return static|null Client instance on success, null if invalid/not found. */ public static function get_by_post_id( $id ) { @@ -310,7 +315,8 @@ public static function get_by_post_id( $id ) { * Create a new client. * * @param array $data { - * } + * } + * * @return WP_Error|Client Client instance on success, error otherwise. */ public static function create( $data ) { @@ -400,6 +406,7 @@ public function approve() { 'post_status' => 'publish', ]; $result = wp_update_post( wp_slash( $data ), true ); + return is_wp_error( $result ) ? $result : true; } @@ -407,25 +414,28 @@ public function approve() { * Register the underlying post type. */ public static function register_type() { - register_post_type( static::POST_TYPE, [ - 'public' => false, - 'hierarchical' => true, - 'capability_type' => [ - 'oauth2_client', - 'oauth2_clients', - ], - 'capabilities' => [ - 'edit_posts' => 'edit_users', - 'edit_others_posts' => 'edit_users', - 'publish_posts' => 'edit_users', - ], - 'supports' => [ - 'title', - 'editor', - 'revisions', - 'author', - 'thumbnail', - ], - ] ); + register_post_type( + static::POST_TYPE, + [ + 'public' => false, + 'hierarchical' => true, + 'capability_type' => [ + 'oauth2_client', + 'oauth2_clients', + ], + 'capabilities' => [ + 'edit_posts' => 'edit_users', + 'edit_others_posts' => 'edit_users', + 'publish_posts' => 'edit_users', + ], + 'supports' => [ + 'title', + 'editor', + 'revisions', + 'author', + 'thumbnail', + ], + ] + ); } } diff --git a/inc/class-clientinterface.php b/inc/class-clientinterface.php index f477ae4..9b42081 100644 --- a/inc/class-clientinterface.php +++ b/inc/class-clientinterface.php @@ -1,4 +1,9 @@ 'POST', - 'callback' => [ $this, 'exchange_token' ], - 'args' => [ - 'grant_type' => [ - 'required' => true, - 'type' => 'string', - 'validate_callback' => [ $this, 'validate_grant_type' ], - ], - 'client_id' => [ - 'required' => true, - 'type' => 'string', - 'validate_callback' => 'rest_validate_request_arg', - ], - 'code' => [ - 'required' => true, - 'type' => 'string', - 'validate_callback' => 'rest_validate_request_arg', + register_rest_route( + 'oauth2', + '/access_token', + [ + 'methods' => 'POST', + 'callback' => [ $this, 'exchange_token' ], + 'args' => [ + 'grant_type' => [ + 'required' => true, + 'type' => 'string', + 'validate_callback' => [ $this, 'validate_grant_type' ], + ], + 'client_id' => [ + 'required' => true, + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ], + 'code' => [ + 'required' => true, + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ], ], - ], - ] ); + ] + ); } /** @@ -43,7 +52,7 @@ public function register_routes() { * @return bool Whether or not the grant type is valid. */ public function validate_grant_type( $type ) { - return $type === 'authorization_code'; + return 'authorization_code' === $type; } /** diff --git a/inc/endpoints/namespace.php b/inc/endpoints/namespace.php index af91775..e5e5049 100644 --- a/inc/endpoints/namespace.php +++ b/inc/endpoints/namespace.php @@ -1,4 +1,9 @@ register_routes(); // Register convenience URL. - register_rest_route( 'oauth2', '/authorize', [ - 'methods' => 'GET', - 'callback' => __NAMESPACE__ . '\\redirect_to_authorize', - ]); + register_rest_route( + 'oauth2', + '/authorize', + [ + 'methods' => 'GET', + 'callback' => __NAMESPACE__ . '\\redirect_to_authorize', + ] + ); } /** diff --git a/inc/namespace.php b/inc/namespace.php index fecbad7..ea681f1 100644 --- a/inc/namespace.php +++ b/inc/namespace.php @@ -1,4 +1,9 @@ $handler ) { if ( ! $handler instanceof Type ) { /* translators: 1: Grant type name, 2: Grant type interface */ - $message = __( 'Skipping invalid grant type "%1$s". Required interface "%1$s" not implemented.', 'oauth2' ); - _doing_it_wrong( __FUNCTION__, sprintf( $message, $type, 'WP\\OAuth2\\Types\\Type' ), '0.1.0' ); + $message = esc_html__( 'Skipping invalid grant type "%1$s". Required interface "%1$s" not implemented.', 'oauth2' ); + _doing_it_wrong( __FUNCTION__, sprintf( esc_html( $message ), esc_html( $type ), 'WP\\OAuth2\\Types\\Type' ), '0.1.0' ); unset( $grant_types[ $type ] ); } } @@ -69,6 +76,7 @@ function get_grant_types() { * Callback for the oauth2.grant_types hook. * * @param array $types Existing grant types. + * * @return array Grant types with additional types registered. */ function register_grant_types( $types ) { @@ -82,6 +90,7 @@ function register_grant_types( $types ) { * Register the OAuth 2 authentication scheme in the API index. * * @param WP_REST_Response $response Index response object. + * * @return WP_REST_Response Update index repsonse object. */ function register_in_index( WP_REST_Response $response ) { @@ -96,6 +105,7 @@ function register_in_index( WP_REST_Response $response ) { ]; $response->set_data( $data ); + return $response; } @@ -136,10 +146,11 @@ function get_token_url() { * Get a client by ID. * * @param string $id ID for the client. + * * @return ClientInterface Client instance. */ function get_client( $id ) { - if ( $id === PersonalClient::ID ) { + if ( PersonalClient::ID === $id ) { return PersonalClient::get_instance(); } diff --git a/inc/tokens/class-access-token.php b/inc/tokens/class-access-token.php index 724aea6..3b6b250 100644 --- a/inc/tokens/class-access-token.php +++ b/inc/tokens/class-access-token.php @@ -1,4 +1,9 @@ get_user_id(), $this->get_meta_key() ); @@ -97,6 +104,7 @@ public function revoke() { * Get a token by ID. * * @param string $id Token ID. + * * @return static|null Token if ID is found, null otherwise. */ public static function get_by_id( $id ) { @@ -107,7 +115,7 @@ public static function get_by_id( $id ) { // We use an EXISTS query here, limited by 1, so we can ignore // the performance warning. - 'meta_query' => [ // WPCS: slow query OK + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query [ 'key' => $key, 'compare' => 'EXISTS', @@ -147,6 +155,7 @@ public static function get_for_user( WP_User $user ) { $value = maybe_unserialize( $values[0] ); $tokens[] = new static( $user, $real_key, $value ); } + return $tokens; } diff --git a/inc/tokens/class-authorization-code.php b/inc/tokens/class-authorization-code.php index fac353c..ac2e41f 100644 --- a/inc/tokens/class-authorization-code.php +++ b/inc/tokens/class-authorization-code.php @@ -1,4 +1,9 @@ user = $user; diff --git a/inc/tokens/namespace.php b/inc/tokens/namespace.php index 0cc8cb0..66a97d5 100644 --- a/inc/tokens/namespace.php +++ b/inc/tokens/namespace.php @@ -1,4 +1,9 @@ filter_redirect_args( $redirect_args, - $submit === 'authorize', + 'authorize' === $submit, $client, $data ); $generated_redirect = add_query_arg( urlencode_deep( $redirect_args ), $redirect_uri ); - wp_redirect( $generated_redirect ); + wp_safe_redirect( $generated_redirect ); exit; } } diff --git a/inc/types/class-base.php b/inc/types/class-base.php index d7bc4ad..733e8d4 100644 --- a/inc/types/class-base.php +++ b/inc/types/class-base.php @@ -1,4 +1,9 @@ get_nonce_action( $client ); - if ( ! wp_verify_nonce( wp_unslash( $_POST['_wpnonce'] ), $nonce_action ) ) { + if ( ! wp_verify_nonce( wp_unslash( $_POST['_wpnonce'] ), $nonce_action ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return new WP_Error( 'oauth2.types.authorization_code.handle_authorisation.invalid_nonce', __( 'Invalid nonce.', 'oauth2' ) @@ -93,7 +103,7 @@ public function handle_authorisation() { return $this->render_form( $client, $error ); } - $submit = wp_unslash( $_POST['wp-submit'] ); + $submit = sanitize_text_field( wp_unslash( $_POST['wp-submit'] ) ); $data = compact( 'redirect_uri', 'scope', 'state' ); return $this->handle_authorization_submission( $submit, $client, $data ); @@ -102,7 +112,7 @@ public function handle_authorisation() { /** * Validate the supplied redirect URI. * - * @param Client $client Client to validate against. + * @param Client $client Client to validate against. * @param string|null $redirect_uri Redirect URI, if supplied. * @return string|WP_Error Valid redirect URI on success, error otherwise. */ @@ -133,7 +143,7 @@ protected function validate_redirect_uri( Client $client, $redirect_uri = null ) /** * Render the authorisation form. * - * @param Client $client Client being authorised. + * @param Client $client Client being authorised. * @param WP_Error $errors Errors to display, if any. */ protected function render_form( Client $client, WP_Error $errors = null ) { @@ -158,10 +168,10 @@ protected function get_nonce_action( Client $client ) { /** * Filter the redirection args. * - * @param array $redirect_args Redirect args. + * @param array $redirect_args Redirect args. * @param boolean $authorized True if authorized, false otherwise. - * @param Client $client Client being authorised. - * @param array $data Data for the request. + * @param Client $client Client being authorised. + * @param array $data Data for the request. */ protected function filter_redirect_args( $redirect_args, $authorized, Client $client, $data ) { if ( ! $authorized ) { diff --git a/inc/types/class-implicit.php b/inc/types/class-implicit.php index 1389b20..a14781e 100644 --- a/inc/types/class-implicit.php +++ b/inc/types/class-implicit.php @@ -1,4 +1,9 @@ filter_redirect_args( $redirect_args, - $submit === 'authorize', + 'authorize' === $submit, $client, $data ); $fragment = build_query( $redirect_args ); $generated_redirect = $redirect_uri . '#' . $fragment; - wp_redirect( $generated_redirect ); + wp_safe_redirect( $generated_redirect ); exit; } diff --git a/inc/types/class-type.php b/inc/types/class-type.php index 36b33e3..d23096b 100644 --- a/inc/types/class-type.php +++ b/inc/types/class-type.php @@ -1,4 +1,9 @@ - - - - 0 - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f00556f..16a3902 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,3 +1,4 @@ + - tests + ./tests/ + ./tests/test-sample.php - - - . - - - ./inc - ./plugin.php - - diff --git a/plugin.php b/plugin.php index 348283b..9b8ae34 100644 --- a/plugin.php +++ b/plugin.php @@ -1,11 +1,26 @@ + * @copyright 2019 Squiz Pty Ltd (ABN 77 084 670 600) + * @license GPL-2.0-or-later + * + * @oauth2 * Plugin Name: OAuth 2 for WordPress + * Plugin URI: https://github.com/WP-API/OAuth2 * Description: Connect apps to your site using OAuth 2. - * Version: 0.1.0 - * Author: WordPress Core Contributors (REST API Focus) - * Author URI: https://make.wordpress.org/core/ + * Version: 0.2.0 + * Author: WordPress Core Contributors (REST API Focus) + * Author URI: https://make.wordpress.org/core/ + * License: GPL v2 or later + * License URI: https://www.gnu.org/licenses/gpl-2.0.html * Text Domain: oauth2 + * Domain Path: /languages + * Requires at least: 4.8 + * Requires PHP: 5.6 */ namespace WP\OAuth2; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 1a95e93..027784f 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,27 +1,31 @@ @@ -60,25 +65,27 @@ %s', - esc_html( sprintf( - /* translators: %1$s: client name */ - __( 'Connect %1$s', 'oauth2' ), - $client->get_name() - ) ) + esc_html( + sprintf( + /* translators: %1$s: client name */ + __( 'Connect %1$s', 'oauth2' ), + $client->get_name() + ) + ) ); ?>