Skip to content

feat: Allow Android WebView admin-ajax.php CORS requests#45292

Closed
dcalhoun wants to merge 5 commits intotrunkfrom
feat/allow-android-web-view-ajax-cors-requests
Closed

feat: Allow Android WebView admin-ajax.php CORS requests#45292
dcalhoun wants to merge 5 commits intotrunkfrom
feat/allow-android-web-view-ajax-cors-requests

Conversation

@dcalhoun
Copy link
Copy Markdown
Member

@dcalhoun dcalhoun commented Sep 24, 2025

Ref CMM-713. Ref CMM-766. Ref CMM-767.

Related: 209198-ghe-Automattic/wpcom

Proposed changes:

The Jetpack mobile app block editor relies upon Android WebViews for serving local Gutenberg files. Editor requests then originate from the https://android-app-assets.jetpack.com origin. To enable admin-ajax.php requests from the Android app, we must allow CORS requests from this origin.

Other information:

  • Have you written new tests for your changes, if applicable?
  • Have you checked the E2E test CI results, and verified that your changes do not break them?
  • Have you tested your changes on WordPress.com, if applicable (if so, you'll see a generated comment below with a script to run)?

Jetpack product discussion

  • pbArwn-7AD-p2
  • p5TWut-1qz-p2

Does this pull request change what data or activity we track or use?

No.

Testing instructions:

Important

WP.com sites require 209198-ghe-Automattic/wpcom as well.

Apply the changes to your site (see comment below). Verify the correct headers are returned for various request structures:

  • Allowed vs disallowed origin
  • Authenticated vs unauthenticated
  • admin-ajax vs non-admin-ajax

An example script is provided below showcasing requests and expectations. The script can be run against a local Jetpack Docker environment site.

  1. Clone this feature branch.
  2. jetpack docker up -d
  3. jetpack docker install
  4. Run the test script below (or perform similar manual requests).
  5. Verify all tests pass.
  6. git switch trunk
  7. Run the test script below (or perform similar manual requests).
  8. Verify test cases 1-2 fail and test cases 3-6 pass.
Example test script

#!/usr/bin/env bash
#
# Test Android WebView CORS support for admin-ajax.php
#
# Uses the Jetpack Docker environment and WordPress Application Passwords.
# Run from anywhere inside the jetpack monorepo.
#
# Usage:
#   ./tools/docker/bin/test-android-cors.sh
#   SITE=http://localhost:8080 WP_USER=admin ./tools/docker/bin/test-android-cors.sh

set -euo pipefail

SITE="${SITE:-http://localhost}"
WP_USER="${WP_USER:-wordpress}"
ALLOWED_ORIGIN="https://android-app-assets.jetpack.com"
EVIL_ORIGIN="https://evil.com"

PASSED=0
FAILED=0
TOTAL=0

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
BOLD='\033[1m'
NC='\033[0m'

##############################################################################
# Helpers
##############################################################################

DIM='\033[2m'

log_group() {
  echo ""
  echo -e "${BOLD}=== $1 ===${NC}"
  echo -e "${DIM}$2${NC}"
}

log_header() {
  echo ""
  echo -e "  ${BOLD}--- $1 ---${NC}"
}

log_pass() {
  ((PASSED++))
  ((TOTAL++))
  echo -e "  ${GREEN}PASS${NC}: $1"
}

log_fail() {
  ((FAILED++))
  ((TOTAL++))
  echo -e "  ${RED}FAIL${NC}: $1"
}

# Fetch response headers into a temp file and check for a header value.
# Usage: header_present "$headers" "Header-Name" "expected-value"
#   Returns 0 if the header is present with the expected value.
header_present() {
  local headers="$1" name="$2" value="$3"
  echo "$headers" | grep -qi "^${name}:.*${value}"
}

# Usage: header_absent "$headers" "Header-Name"
#   Returns 0 if the header is NOT present at all.
header_absent() {
  local headers="$1" name="$2"
  ! echo "$headers" | grep -qi "^${name}:"
}

##############################################################################
# Setup: ensure the site is reachable and create an Application Password
##############################################################################

log_group "Setup" "Verify Docker environment and create temporary Application Password"

log_header "Preflight check"
if ! curl -s --max-time 5 -o /dev/null -w '' "${SITE}/" 2>/dev/null; then
  echo -e "${RED}Cannot reach ${SITE}. Is the Docker environment running?${NC}"
  echo "Start it with: pnpm jetpack docker up"
  exit 1
fi
echo -e "  Site ${SITE} is reachable."

log_header "Creating Application Password"

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
JETPACK_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"

# Run WP-CLI inside the Docker container via pnpm jetpack docker wp.
jetpack_wp() {
  pnpm --silent --dir="${JETPACK_ROOT}" jetpack docker wp -- "$@"
}

# Create app password via WP-CLI, capture the plaintext password.
APP_PASS_NAME="cors-test-$(date +%s)"
if ! APP_PASS_RAW=$(jetpack_wp user application-password create "${WP_USER}" "${APP_PASS_NAME}" --porcelain 2>&1); then
  echo -e "${RED}Failed to create Application Password:${NC}"
  echo "  $APP_PASS_RAW"
  exit 1
fi
APP_PASS=$(echo "$APP_PASS_RAW" | tr -d '[:space:]')

if [[ -z "$APP_PASS" ]]; then
  echo -e "${RED}Failed to create Application Password. Is the Docker container running?${NC}"
  exit 1
fi
echo "  Application Password created."

# Build Basic auth header value.
AUTH_HEADER="$(echo -n "${WP_USER}:${APP_PASS}" | base64)"

# Cleanup: delete the app password on exit.
cleanup() {
  jetpack_wp user application-password delete "${WP_USER}" --all > /dev/null 2>&1 || true
  echo ""
  echo "  Application Password cleaned up."
}
trap cleanup EXIT

##############################################################################
# Positive tests: CORS headers ARE expected for authorized requests from
# the allowed Android WebView origin. These validate the new behavior
# added by this branch — they should fail on trunk.
##############################################################################

log_group "Positive tests" "CORS headers expected for authorized requests from the allowed origin (should fail on trunk)"

log_header "Test 1: Preflight (OPTIONS) with allowed origin + Authorization header requested"

HEADERS=$(curl -sS -D - -o /dev/null -X OPTIONS \
  "${SITE}/wp-admin/admin-ajax.php?action=videopress-get-upload-jwt" \
  -H "Origin: ${ALLOWED_ORIGIN}" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Authorization, Content-Type" 2>&1)

if header_present "$HEADERS" "Access-Control-Allow-Origin" "$ALLOWED_ORIGIN"; then
  log_pass "Access-Control-Allow-Origin present with allowed origin"
else
  log_fail "Access-Control-Allow-Origin missing or wrong"
fi

if header_present "$HEADERS" "Access-Control-Allow-Headers" "Authorization"; then
  log_pass "Access-Control-Allow-Headers includes Authorization"
else
  log_fail "Access-Control-Allow-Headers missing Authorization"
fi

if header_present "$HEADERS" "Access-Control-Allow-Methods" "POST"; then
  log_pass "Access-Control-Allow-Methods includes POST"
else
  log_fail "Access-Control-Allow-Methods missing POST"
fi

if header_present "$HEADERS" "Access-Control-Max-Age" "86400"; then
  log_pass "Access-Control-Max-Age is 86400"
else
  log_fail "Access-Control-Max-Age missing or wrong"
fi

##############################################################################
# Test 2: POST with Authorization from allowed origin
##############################################################################

log_header "Test 2: POST with Authorization from allowed origin"

HEADERS=$(curl -sS -D - -o /dev/null -X POST \
  "${SITE}/wp-admin/admin-ajax.php" \
  -H "Origin: ${ALLOWED_ORIGIN}" \
  -H "Authorization: Basic ${AUTH_HEADER}" \
  -d "action=videopress-get-upload-jwt" 2>&1)

if header_present "$HEADERS" "Access-Control-Allow-Origin" "$ALLOWED_ORIGIN"; then
  log_pass "Access-Control-Allow-Origin present for authenticated request"
else
  log_fail "Access-Control-Allow-Origin missing for authenticated request"
fi

##############################################################################
# Negative tests: CORS headers should NOT be present for disallowed origins,
# unauthenticated requests, non-auth preflights, or non-admin-ajax endpoints.
# These guard against over-permissive CORS and should pass on any branch.
##############################################################################

log_group "Negative tests" "CORS headers must be absent for unauthorized or disallowed requests (should pass on any branch)"

log_header "Test 3: POST with Authorization from disallowed origin"

HEADERS=$(curl -sS -D - -o /dev/null -X POST \
  "${SITE}/wp-admin/admin-ajax.php" \
  -H "Origin: ${EVIL_ORIGIN}" \
  -H "Authorization: Basic ${AUTH_HEADER}" \
  -d "action=videopress-get-upload-jwt" 2>&1)

if header_absent "$HEADERS" "Access-Control-Allow-Origin"; then
  log_pass "Access-Control-Allow-Origin absent for disallowed origin"
else
  # WordPress core may echo the origin via send_origin_headers if it's in
  # allowed_http_origins. We just need to confirm it is NOT the evil origin.
  if header_present "$HEADERS" "Access-Control-Allow-Origin" "$EVIL_ORIGIN"; then
    log_fail "Access-Control-Allow-Origin present for disallowed origin"
  else
    log_pass "Access-Control-Allow-Origin not set to disallowed origin"
  fi
fi

##############################################################################
# Test 4: POST without Authorization from allowed origin
##############################################################################

log_header "Test 4: POST without Authorization from allowed origin"

HEADERS=$(curl -sS -D - -o /dev/null -X POST \
  "${SITE}/wp-admin/admin-ajax.php" \
  -H "Origin: ${ALLOWED_ORIGIN}" \
  -d "action=videopress-get-upload-jwt" 2>&1)

if header_absent "$HEADERS" "Access-Control-Allow-Origin"; then
  log_pass "Access-Control-Allow-Origin absent without Authorization"
else
  if header_present "$HEADERS" "Access-Control-Allow-Origin" "$ALLOWED_ORIGIN"; then
    log_fail "Access-Control-Allow-Origin should not be set without Authorization"
  else
    log_pass "Access-Control-Allow-Origin not set to Android origin without auth"
  fi
fi

##############################################################################
# Test 5: OPTIONS preflight without Authorization in requested headers
##############################################################################

log_header "Test 5: Preflight (OPTIONS) without Authorization in requested headers"

HEADERS=$(curl -sS -D - -o /dev/null -X OPTIONS \
  "${SITE}/wp-admin/admin-ajax.php?action=videopress-get-upload-jwt" \
  -H "Origin: ${ALLOWED_ORIGIN}" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type" 2>&1)

if header_absent "$HEADERS" "Access-Control-Allow-Methods"; then
  log_pass "Custom preflight headers absent when Authorization not requested"
else
  log_fail "Custom preflight headers should not appear without Authorization in request headers"
fi

##############################################################################
# Test 6: Non-admin-ajax endpoint should not get CORS headers
##############################################################################

log_header "Test 6: Non-admin-ajax endpoint (wp-login.php)"

HEADERS=$(curl -sS -D - -o /dev/null -X POST \
  "${SITE}/wp-login.php" \
  -H "Origin: ${ALLOWED_ORIGIN}" \
  -H "Authorization: Basic ${AUTH_HEADER}" 2>&1)

if header_absent "$HEADERS" "Access-Control-Allow-Origin"; then
  log_pass "Access-Control-Allow-Origin absent for non-admin-ajax endpoint"
else
  if header_present "$HEADERS" "Access-Control-Allow-Origin" "$ALLOWED_ORIGIN"; then
    log_fail "Access-Control-Allow-Origin should not appear for non-admin-ajax endpoint"
  else
    log_pass "Access-Control-Allow-Origin not set to Android origin for non-admin-ajax"
  fi
fi

##############################################################################
# Summary
##############################################################################

echo ""
echo -e "${BOLD}────────────────────────────────${NC}"
echo -e "${BOLD}Results: ${PASSED}/${TOTAL} passed${NC}"
if [[ $FAILED -gt 0 ]]; then
  echo -e "${RED}${FAILED} test(s) failed${NC}"
  exit 1
else
  echo -e "${GREEN}All tests passed${NC}"
fi

@dcalhoun dcalhoun added Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Status] In Progress [Feature] WP REST API [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ labels Sep 24, 2025
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Sep 24, 2025

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the feat/allow-android-web-view-ajax-cors-requests branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack feat/allow-android-web-view-ajax-cors-requests

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Sep 24, 2025

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Jetpack plugin:

The Jetpack plugin has different release cadences depending on the platform:

  • WordPress.com Simple releases happen as soon as you deploy your changes after merging this PR (PCYsg-Jjm-p2).
  • WoA releases happen weekly.
  • Releases to self-hosted sites happen monthly:
    • Scheduled release: March 31, 2026
    • Code freeze: March 31, 2026

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@jp-launch-control
Copy link
Copy Markdown

jp-launch-control bot commented Sep 24, 2025

Code Coverage Summary

Coverage changed in 1 file.

File Coverage Δ% Δ Uncovered
projects/plugins/jetpack/_inc/lib/class-jetpack-application-password-extras.php 34/39 (87.18%) 2.56% 3 ❤️‍🩹

Full summary · PHP report · JS report

@dcalhoun dcalhoun force-pushed the feat/editor-assets-endpoint-allows-videopress branch from 7c65124 to 0eec284 Compare November 26, 2025 17:59
@dcalhoun dcalhoun force-pushed the feat/allow-android-web-view-ajax-cors-requests branch from 1e87e88 to 947871d Compare November 26, 2025 17:59
@dcalhoun dcalhoun force-pushed the feat/editor-assets-endpoint-allows-videopress branch from 0eec284 to 2ebdfb2 Compare January 12, 2026 15:39
@dcalhoun dcalhoun force-pushed the feat/allow-android-web-view-ajax-cors-requests branch from 947871d to 5c0702b Compare January 12, 2026 15:40
@dcalhoun dcalhoun force-pushed the feat/editor-assets-endpoint-allows-videopress branch from 2ebdfb2 to b5d50be Compare January 12, 2026 18:06
@dcalhoun dcalhoun force-pushed the feat/allow-android-web-view-ajax-cors-requests branch from 5c0702b to 0cc2139 Compare January 12, 2026 18:06
@dcalhoun dcalhoun force-pushed the feat/editor-assets-endpoint-allows-videopress branch from b5d50be to d56e487 Compare January 13, 2026 16:48
@dcalhoun dcalhoun force-pushed the feat/allow-android-web-view-ajax-cors-requests branch from 46ba2ef to 5d316d9 Compare January 13, 2026 16:49
@dcalhoun dcalhoun force-pushed the feat/editor-assets-endpoint-allows-videopress branch from d56e487 to cf08962 Compare January 13, 2026 17:22
@dcalhoun dcalhoun force-pushed the feat/allow-android-web-view-ajax-cors-requests branch from 5d316d9 to 67fa677 Compare January 13, 2026 17:22
@dcalhoun dcalhoun force-pushed the feat/editor-assets-endpoint-allows-videopress branch from cf08962 to ae35d6d Compare February 12, 2026 16:51
@dcalhoun dcalhoun force-pushed the feat/allow-android-web-view-ajax-cors-requests branch from 67fa677 to a374c90 Compare February 12, 2026 16:51
@dcalhoun dcalhoun force-pushed the feat/editor-assets-endpoint-allows-videopress branch from ae35d6d to 8dd337f Compare February 13, 2026 15:18
@dcalhoun dcalhoun force-pushed the feat/allow-android-web-view-ajax-cors-requests branch from a374c90 to a599d22 Compare February 13, 2026 15:19
@dcalhoun dcalhoun force-pushed the feat/allow-android-web-view-ajax-cors-requests branch from a599d22 to 54ef71f Compare March 10, 2026 14:53
@dcalhoun dcalhoun changed the base branch from feat/editor-assets-endpoint-allows-videopress to feat/expand-application-passwords-abilities March 10, 2026 14:53
Base automatically changed from feat/expand-application-passwords-abilities to trunk March 17, 2026 15:04
The Jetpack mobile app block editor relies upon Android WebViews for
serving local Gutenberg files. Editor requests then originate from
the platform-default `https://appassets.androidplatform.net` origin. To
enable `admin-ajax.php` requests from the Android app, we must allow
CORS requests from this origin.
Use a domain owned by the Jetpack project.
The domain was updated in a previous commit.
Re-run CI tasks after changing GitHub PR base branch.
@dcalhoun dcalhoun force-pushed the feat/allow-android-web-view-ajax-cors-requests branch from 275dc4a to 3c95750 Compare March 24, 2026 17:37
@dcalhoun dcalhoun marked this pull request as ready for review March 24, 2026 20:27
@dcalhoun dcalhoun added [Status] Needs Review This PR is ready for review. and removed [Status] In Progress labels Mar 24, 2026
@dcalhoun dcalhoun requested a review from enejb March 24, 2026 20:29
@dcalhoun
Copy link
Copy Markdown
Member Author

Closing this as I now believe it unnecessary. It seems possible to avoid CORS altogether by using the site's domain for loading the local assets in the WebView.

Sorry for the noise.

@dcalhoun dcalhoun closed this Mar 28, 2026
@dcalhoun dcalhoun removed the request for review from enejb March 28, 2026 00:57
@dcalhoun dcalhoun deleted the feat/allow-android-web-view-ajax-cors-requests branch March 28, 2026 00:57
@github-actions github-actions bot removed the [Status] Needs Review This PR is ready for review. label Mar 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Feature] WP REST API [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant