Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions includes/list-tables/class-test-runs-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,10 @@ public function column_trigger( $item ) {
'<span class="vrts-test-run-trigger vrts-test-run-trigger--%s">%s</span>%s',
esc_attr( $item->trigger ),
esc_html( $trigger_title ),
empty( $trigger_note ) ? '' : sprintf('<p class="vrts-test-run-trigger-notes" title="%1$s">%1$s</p>',
$trigger_note
empty( $trigger_note ) ? '' : sprintf(
'<p class="vrts-test-run-trigger-notes" title="%1$s">%2$s</p>',
esc_attr( $trigger_note ),
esc_html( $trigger_note )
)
);
}
Expand Down
5 changes: 3 additions & 2 deletions includes/list-tables/class-test-runs-queue-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,11 @@ public function column_trigger( $item ) {
$trigger_note = Test_Run::get_trigger_note( $item );

return sprintf(
'<span class="vrts-test-run-trigger vrts-test-run-trigger--%s">%s</span><p class="vrts-test-run-trigger-notes" title="%3$s">%3$s</p>',
'<span class="vrts-test-run-trigger vrts-test-run-trigger--%s">%s</span><p class="vrts-test-run-trigger-notes" title="%s">%s</p>',
esc_attr( $item->trigger ),
esc_html( $trigger_title ),
$trigger_note
esc_attr( $trigger_note ),
esc_html( $trigger_note )
);
}

Expand Down
108 changes: 70 additions & 38 deletions includes/rest-api/class-rest-service-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function register_routes() {
public function ajax_action() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- It's ok.
$data = json_decode( wp_unslash( $_REQUEST['data'] ?? '' ), true );
$rest_response = $this->perform_action( $data ?? [] );
$rest_response = $this->perform_action( is_array( $data ) ? $data : [] );

// If rest response is WP error, get the status code.
if ( is_wp_error( $rest_response ) ) {
Expand Down Expand Up @@ -86,6 +86,22 @@ public function perform_action( $data ) {
return new WP_Error( 'error', esc_html__( 'Action parameter is missing.', 'visual-regression-tests' ), [ 'status' => 403 ] );
}

$allowed_actions = [
'test_updated',
'run_updated',
'run_deleted',
'subscription_changed',
];

if ( ! is_string( $data['action'] ) || ! in_array( $data['action'], $allowed_actions, true ) ) {
return $this->unknown_action_request();
}

$authentication = $this->authenticate_request( $data );
if ( is_wp_error( $authentication ) ) {
return $authentication;
}

switch ( $data['action'] ) {

case 'test_updated':
Expand All @@ -103,10 +119,6 @@ public function perform_action( $data ) {
case 'subscription_changed':
$response = $this->subscription_changed_request();
break;

default:
$response = $this->unknown_action_request();
break;
}//end switch

return $response;
Expand All @@ -118,18 +130,10 @@ public function perform_action( $data ) {
* @param array $data Rest api response body.
*/
private function test_updated_request( $data ) {
if ( ! array_key_exists( 'project_id', $data ) ) {
return new WP_Error( 'error', esc_html__( 'Project id is missing.', 'visual-regression-tests' ), [ 'status' => 403 ] );
} elseif ( get_option( 'vrts_project_id' ) !== $data['project_id'] ) {
return new WP_Error( 'error', esc_html__( 'Project id does not match.', 'visual-regression-tests' ), [ 'status' => 403 ] );
} elseif ( ! array_key_exists( 'test_id', $data ) ) {
if ( ! array_key_exists( 'test_id', $data ) ) {
return new WP_Error( 'error', esc_html__( 'Test id is missing.', 'visual-regression-tests' ), [ 'status' => 403 ] );
}

if ( ! self::verify_signature( $data ) ) {
return new WP_Error( 'error', esc_html__( 'Signature is not valid.', 'visual-regression-tests' ), [ 'status' => 403 ] );
}

$test_service = new Test_Service();
if ( $test_service->update_test_from_api_data( $data ) ) {

Expand All @@ -149,18 +153,10 @@ private function test_updated_request( $data ) {
* @param array $data Rest api response body.
*/
private function run_updated_request( $data ) {
if ( ! array_key_exists( 'project_id', $data ) ) {
return new WP_Error( 'error', esc_html__( 'Project id is missing.', 'visual-regression-tests' ), [ 'status' => 403 ] );
} elseif ( get_option( 'vrts_project_id' ) !== $data['project_id'] ) {
return new WP_Error( 'error', esc_html__( 'Project id does not match.', 'visual-regression-tests' ), [ 'status' => 403 ] );
} elseif ( ! array_key_exists( 'run_id', $data ) ) {
if ( ! array_key_exists( 'run_id', $data ) ) {
return new WP_Error( 'error', esc_html__( 'Run id is missing.', 'visual-regression-tests' ), [ 'status' => 403 ] );
}

if ( ! self::verify_signature( $data ) ) {
return new WP_Error( 'error', esc_html__( 'Signature is not valid.', 'visual-regression-tests' ), [ 'status' => 403 ] );
}

$test_run_service = new Test_Run_Service();
if ( $test_run_service->update_run_from_api_data( $data ) ) {

Expand All @@ -180,18 +176,10 @@ private function run_updated_request( $data ) {
* @param array $data Rest api response body.
*/
private function run_deleted_request( $data ) {
if ( ! array_key_exists( 'project_id', $data ) ) {
return new WP_Error( 'error', esc_html__( 'Project id is missing.', 'visual-regression-tests' ), [ 'status' => 403 ] );
} elseif ( get_option( 'vrts_project_id' ) !== $data['project_id'] ) {
return new WP_Error( 'error', esc_html__( 'Project id does not match.', 'visual-regression-tests' ), [ 'status' => 403 ] );
} elseif ( ! array_key_exists( 'run_id', $data ) ) {
if ( ! array_key_exists( 'run_id', $data ) ) {
return new WP_Error( 'error', esc_html__( 'Run id is missing.', 'visual-regression-tests' ), [ 'status' => 403 ] );
}

if ( ! self::verify_signature( $data ) ) {
return new WP_Error( 'error', esc_html__( 'Signature is not valid.', 'visual-regression-tests' ), [ 'status' => 403 ] );
}

$test_run_service = new Test_Run_Service();
if ( Test_Run::delete_by_service_test_run_id( $data['run_id'] ) ) {

Expand All @@ -204,19 +192,63 @@ private function run_deleted_request( $data ) {
}

/**
* Verify signature
* Authenticate a service callback.
*
* @param array $data Rest api response body.
*
* @return bool|WP_Error
*/
private function authenticate_request( $data ) {
$project_id = get_option( 'vrts_project_id' );
$secret = get_option( 'vrts_project_secret' );

if ( ! is_string( $project_id ) || '' === $project_id || ! is_string( $secret ) || '' === $secret ) {
return new WP_Error( 'error', esc_html__( 'Service is not configured.', 'visual-regression-tests' ), [ 'status' => 403 ] );
}

if ( ! array_key_exists( 'project_id', $data ) ) {
return new WP_Error( 'error', esc_html__( 'Project id is missing.', 'visual-regression-tests' ), [ 'status' => 403 ] );
}

if ( ! is_string( $data['project_id'] ) || $project_id !== $data['project_id'] ) {
return new WP_Error( 'error', esc_html__( 'Project id does not match.', 'visual-regression-tests' ), [ 'status' => 403 ] );
}

if ( ! $this->verify_signature( $data, $secret ) ) {
return new WP_Error( 'error', esc_html__( 'Signature is not valid.', 'visual-regression-tests' ), [ 'status' => 403 ] );
}

return true;
}

/**
* Verify signature.
*
* The service dual-signs callbacks during the transition away from the
* broken pre-2.0.9 scheme: `signature` carries the legacy HMAC for old
* plugins, `signature_v2` the HMAC keyed with the real project secret,
* computed over the payload without either signature field.
*
* @param array $data Rest api response body.
* @param string $secret Project signing secret.
*
* @return bool
*/
private function verify_signature( $data ) {
$signature = $data['signature'];
unset( $data['signature'] );
private function verify_signature( $data, $secret ) {
if (
! array_key_exists( 'signature_v2', $data )
|| ! is_string( $data['signature_v2'] )
|| 1 !== preg_match( '/^[a-f0-9]{64}$/', $data['signature_v2'] )
) {
return false;
}

$signature = $data['signature_v2'];
unset( $data['signature'], $data['signature_v2'] );

$secret = get_option( 'vrts_project_secret' ) || 'verysecret';
$expected_signature = hash_hmac( 'sha256', wp_json_encode( $data ), $secret );

return hash_equals( $signature, hash_hmac( 'sha256', wp_json_encode( $data ), $secret ) );
return hash_equals( $expected_signature, $signature );
}

/**
Expand Down
Loading