Skip to content

Conversation

@dcalhoun
Copy link
Member

@dcalhoun dcalhoun commented Sep 19, 2025

Related:

What?

Authenticate AJAX requests with application passwords send via an authorization
header.

Why?

The GutenbergKit editor does not have authorization cookies, so we must rely
upon a different authorization mechanism.

Ref CMM-713. Close CMM-768.

How?

Set the Authorization header via jQuery.ajaxSetup and by overloading the
window.wp.ajax utilities.

Testing Instructions

  1. Apply the required Jetpack changes and WPCOM (192792-ghe-Automattic/wpcom) changes to your site.
  2. Proxy your test device's networking through to your modified WPCOM in step 1.
  3. Open the experimental block editor in the Jetpack mobile app for Android.
  4. Insert a VideoPress block.
  5. Attach media.
  6. Verify the upload succeeds.

Accessibility Testing Instructions

N/A, no navigation changes.

Screenshots or screencast

N/A, no visual changes.

@dcalhoun dcalhoun added the [Type] Enhancement A suggestion for improvement. label Sep 19, 2025
@dcalhoun dcalhoun force-pushed the feat/authorize-ajax-with-application-passwords branch from a051ea4 to 682e0df Compare September 19, 2025 20:00
@dcalhoun dcalhoun changed the title feat: Authorize AJAX with application passwords feat: Authorize AJAX with token authentication Sep 24, 2025
@dcalhoun dcalhoun changed the title feat: Authorize AJAX with token authentication feat: AJAX uses token authentication Sep 25, 2025
@dcalhoun dcalhoun force-pushed the feat/authorize-ajax-with-application-passwords branch from 686ebf7 to bbaf9c4 Compare January 13, 2026 19:35
@dcalhoun
Copy link
Member Author

@claude

@claude
Copy link

claude bot commented Jan 13, 2026

Claude finished @dcalhoun's task in 4m 47s —— View job


Code Review Complete ✅

I've completed a comprehensive review of PR #181. Overall, this is a well-implemented and thoroughly tested feature that successfully replaces the VideoPress-specific bridge with a general AJAX authentication solution. The code quality is high, security is properly handled, and the documentation is excellent.


Summary

This PR introduces token-based authentication for AJAX requests via authorization headers, replacing the previous VideoPress-specific bridge (videopress-bridge.js) with a more general and robust solution (ajax.js). The implementation properly configures both jQuery AJAX and WordPress AJAX utilities (wp.ajax.send and wp.ajax.post) to inject authentication headers.

Key Changes:

  • ✅ New ajax.js module with comprehensive AJAX configuration
  • ✅ Excellent test coverage (491 lines of tests)
  • ✅ Android configuration updated to support authHeader and assetLoaderDomain
  • ✅ Comprehensive documentation for AJAX requirements
  • ✅ Removed VideoPress-specific bridge in favor of general solution

Detailed Findings

1. Code Quality ⭐⭐⭐⭐⭐

Strengths:

  • Clean, readable code with clear function separation
  • Excellent use of optional chaining (window.jQuery?.ajaxSetup)
  • Proper error handling with warning logs
  • Good function ordering following repository conventions
  • Well-structured with helper functions (configureAjaxUrl, configureAjaxAuth)

Minor Observations:

  • The code correctly preserves existing beforeSend callbacks when wrapping wp.ajax.send and wp.ajax.post
  • Defensive programming with checks for missing siteURL and authHeader
  • Uses project's logger utility instead of console methods ✅

2. Security ⭐⭐⭐⭐⭐

Strengths:

  • ✅ Authorization headers properly injected into all AJAX requests
  • ✅ No credential exposure in logs (tokens not logged)
  • ✅ Android properly validates and passes authHeader through configuration
  • ✅ Token authentication is more secure than cookie-based auth for native WebViews
  • ✅ CORS properly documented and configured for Android's assetLoaderDomain

Observations:

  • The authHeader is expected to be pre-formatted (e.g., "Bearer token") - this is appropriate since token formatting is the caller's responsibility
  • Authorization headers are set via both jQuery global setup and per-request injection for wp.ajax methods, ensuring comprehensive coverage

3. Performance ⭐⭐⭐⭐⭐

Strengths:

  • ✅ Minimal overhead - configuration happens once at initialization
  • ✅ No unnecessary network requests or polling
  • ✅ Function wrapping uses closures efficiently
  • ✅ Proper use of optional chaining avoids unnecessary checks

Observations:

  • The wrapping of wp.ajax.send and wp.ajax.post adds one additional function call per request, which is negligible
  • Configuration is called once in initializeEditor (src/utils/editor-environment.js:141)

4. Testing ⭐⭐⭐⭐⭐

Strengths:

  • Exceptional test coverage with 491 lines of tests
  • ✅ Tests cover all scenarios:
    • URL configuration with and without siteURL
    • Auth configuration with and without authHeader
    • jQuery integration
    • wp.ajax.send and wp.ajax.post wrapping
    • Preservation of original beforeSend callbacks
    • Edge cases (missing jQuery, missing methods, partial initialization)
    • Integration scenarios
  • ✅ Uses proper mocking with Vitest
  • ✅ Tests verify both success and error paths
  • ✅ Good test organization with descriptive test names

Observations:

  • Test coverage appears comprehensive based on the implementation
  • Tests properly verify that authorization headers are set correctly
  • Tests verify that existing beforeSend callbacks are preserved

5. Documentation ⭐⭐⭐⭐⭐

Strengths:

  • Excellent documentation in docs/integration.md covering:
    • Requirements for AJAX support
    • Production bundle requirement (CORS limitations)
    • Platform-specific configuration differences clearly explained
    • Server-side CORS configuration notes
  • ✅ Troubleshooting guide updated with AJAX CORS error documentation
  • ✅ Code comments explain the purpose and behavior clearly
  • ✅ JSDoc comments for public functions

Observations:

  • The documentation correctly notes that development server doesn't support AJAX due to CORS
  • Android-specific assetLoaderDomain requirement is well-documented
  • Documentation provides concrete code examples for both iOS and Android

Issues Found

Critical Issues

None

High Priority Issues

None

Medium Priority Issues

1. Potential Race Condition with wp.ajax Methods

Location: src/utils/ajax.js:49-77

The code wraps window.wp.ajax.send and window.wp.ajax.post, but these methods might not exist at the time configureAjax() is called (lines 49-77). While the tests verify this scenario doesn't crash during configuration, the code doesn't check if the methods exist before wrapping them.

Current behavior:

const originalSend = window.wp.ajax.send; // Could be undefined
window.wp.ajax.send = function(options) {
    // ...
    return originalSend.call(this, options); // Will fail if originalSend is undefined
};

Impact: If window.wp.ajax.send is undefined when configureAjax() runs, the wrapped function will work fine until it's actually invoked, at which point it will throw TypeError: originalSend.call is not a function.

Recommendation: Add existence checks before wrapping:

if (typeof window.wp.ajax.send === 'function') {
    const originalSend = window.wp.ajax.send;
    window.wp.ajax.send = function(options) { ... };
}

Test Gap: Test on line 390 verifies this doesn't throw during configuration, but doesn't test what happens when the wrapped method is actually invoked with originalSend being undefined.

See inline comment: src/utils/ajax.js:49-61

Low Priority Issues

2. Redundant Authorization Header Setting

Location: src/utils/ajax.js:43-47, 49-77

The code sets authorization headers via jQuery.ajaxSetup() AND by wrapping wp.ajax.send/wp.ajax.post. For wp.ajax methods, this means the authorization header is set twice.

Impact: Minimal - setting the same header twice is harmless. This provides defense-in-depth and ensures the auth header is present even if one mechanism fails.

Recommendation: This is fine as-is. The redundancy is actually beneficial for robustness.

See inline comment: src/utils/ajax.js:43-47


Positive Highlights 🌟

Excellent Architecture

  • Replacing the VideoPress-specific bridge with a general AJAX solution is architecturally superior
  • Enables any WordPress blocks/plugins that use AJAX to work in GutenbergKit
  • Clean separation of concerns between URL configuration and auth configuration

Outstanding Test Coverage

  • 491 lines of comprehensive tests covering all edge cases
  • Tests for missing jQuery, undefined methods, partial initialization
  • Integration tests verify the complete flow
  • See inline comment: src/utils/ajax.test.js:355-491

Great Android Integration

Comprehensive Documentation

  • Clear explanation of AJAX requirements and limitations
  • Platform-specific differences well-documented
  • Troubleshooting guide includes AJAX-specific errors
  • Code examples provided for both iOS and Android

Recommendations

Must Fix Before Merge

None - the code is production-ready as-is. The medium-priority issue is an edge case that may never occur in practice.

Should Consider

  1. ⚠️ Add existence checks before wrapping wp.ajax.send and wp.ajax.post methods (see inline comment)
  2. Add test for wrapped method invocation when original method is undefined

Nice to Have

  1. Document the timing of AJAX configuration in code comments
  2. Add comment explaining redundant header setting is intentional for defense in depth
  3. Consider adding a reconfigureAjax() function for token updates (if needed in future)

Conclusion

This is a high-quality PR that successfully implements token authentication for AJAX requests. The code is well-written, thoroughly tested, and properly documented. The architectural decision to replace the VideoPress-specific bridge with a general solution is excellent.

Scores

  • Code Quality: ⭐⭐⭐⭐⭐ 5/5
  • Security: ⭐⭐⭐⭐⭐ 5/5
  • Performance: ⭐⭐⭐⭐⭐ 5/5
  • Testing: ⭐⭐⭐⭐⭐ 5/5
  • Documentation: ⭐⭐⭐⭐⭐ 5/5

Recommendation

Approve with minor suggestions - The identified issues are minor and don't block merging. The PR is production-ready, and the suggestions above would make the code slightly more robust but aren't critical for functionality.

The only medium-priority issue (wrapping potentially undefined methods) is an edge case that may never occur in practice since WordPress globals are loaded before AJAX configuration runs.


Files Reviewed

  • src/utils/ajax.js - Core AJAX configuration
  • src/utils/ajax.test.js - Comprehensive test suite (491 lines)
  • src/utils/editor-environment.js - Integration point
  • src/utils/editor-environment.test.js - Integration tests
  • android/Gutenberg/src/main/java/org/wordpress/gutenberg/EditorConfiguration.kt - Android config
  • android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt - Android WebView
  • docs/integration.md - AJAX documentation (40 new lines)
  • docs/code/troubleshooting.md - AJAX troubleshooting
  • src/utils/videopress-bridge.js - Deleted (replaced by ajax.js)
  • ios/Sources/GutenbergKit/Sources/Model/GBKitGlobal.swift - iOS configuration (verified authHeader support)

| Branch: feat/authorize-ajax-with-application-passwords

Comment on lines +43 to +47
window.jQuery?.ajaxSetup( {
headers: {
Authorization: authHeader,
},
} );
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor observation: This sets the authorization header globally for all jQuery AJAX requests. Combined with the per-request header setting in the wp.ajax.send/wp.ajax.post wrappers below, this means wp.ajax requests will have the header set twice.

This is harmless and provides defense-in-depth, but worth noting. The redundancy ensures the auth header is present even if one mechanism fails.

Comment on lines +26 to +27
val enableNetworkLogging: Boolean = false,
val assetLoaderDomain: String? = null
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great addition! The assetLoaderDomain configuration properly addresses AJAX CORS requirements for Android. The nullable type with null default is correct since it falls back to DEFAULT_ASSET_DOMAIN if not specified (GutenbergView.kt:262).

The documentation in docs/integration.md clearly explains that this must be set to a domain allowed by the WordPress site's CORS policy for AJAX to work on Android.

Comment on lines 355 to 491
describe( 'Edge cases', () => {
it( 'should handle missing jQuery gracefully', () => {
bridge.getGBKit.mockReturnValue( {
siteURL: 'https://example.com',
authHeader: 'Bearer no-jquery',
} );

delete global.window.jQuery;

expect( () => configureAjax() ).not.toThrow();
expect( logger.debug ).toHaveBeenCalledWith(
'AJAX URL configured'
);
expect( logger.debug ).toHaveBeenCalledWith(
'AJAX auth configured'
);
} );

it( 'should handle undefined jQuery', () => {
bridge.getGBKit.mockReturnValue( {
siteURL: 'https://example.com',
authHeader: 'Bearer undefined-jquery',
} );

global.window.jQuery = undefined;

expect( () => configureAjax() ).not.toThrow();
expect( logger.debug ).toHaveBeenCalledWith(
'AJAX URL configured'
);
expect( logger.debug ).toHaveBeenCalledWith(
'AJAX auth configured'
);
} );

it( 'should handle missing wp.ajax.send method', () => {
bridge.getGBKit.mockReturnValue( {
siteURL: 'https://example.com',
authHeader: 'Bearer no-send',
} );

global.window.wp = {
ajax: {
post: originalWpAjaxPost,
settings: {},
},
};

expect( () => configureAjax() ).not.toThrow();

// Should still wrap post
expect( global.window.wp.ajax.post ).not.toBe( originalWpAjaxPost );
} );

it( 'should handle missing wp.ajax.post method', () => {
bridge.getGBKit.mockReturnValue( {
siteURL: 'https://example.com',
authHeader: 'Bearer no-post',
} );

global.window.wp = {
ajax: {
send: originalWpAjaxSend,
settings: {},
},
};

expect( () => configureAjax() ).not.toThrow();

// Should still wrap send
expect( global.window.wp.ajax.send ).not.toBe( originalWpAjaxSend );
} );

it( 'should handle missing wp.ajax entirely', () => {
bridge.getGBKit.mockReturnValue( {
siteURL: 'https://example.com',
authHeader: 'Bearer no-ajax',
} );

global.window.wp = {};

expect( () => configureAjax() ).not.toThrow();

// Should create ajax object
expect( global.window.wp.ajax ).toBeDefined();
expect( global.window.wp.ajax.settings ).toBeDefined();
} );

it( 'should work with window.wp already partially initialized', () => {
bridge.getGBKit.mockReturnValue( {
siteURL: 'https://example.com',
authHeader: null,
} );

// Pre-existing wp object with other properties
global.window.wp = {
data: { someData: 'test' },
};

configureAjax();

// Should preserve existing properties
expect( global.window.wp.data ).toEqual( { someData: 'test' } );

// Should add ajax properties
expect( global.window.wp.ajax ).toBeDefined();
expect( global.window.wp.ajax.settings.url ).toBe(
'https://example.com/wp-admin/admin-ajax.php'
);
} );

it( 'should work when wp.ajax is partially initialized', () => {
bridge.getGBKit.mockReturnValue( {
siteURL: 'https://example.com',
authHeader: null,
} );

// Pre-existing wp.ajax object without settings
global.window.wp = {
ajax: {
someMethod: vi.fn(),
},
};

configureAjax();

// Should preserve existing methods
expect( global.window.wp.ajax.someMethod ).toBeDefined();

// Should add settings
expect( global.window.wp.ajax.settings ).toBeDefined();
expect( global.window.wp.ajax.settings.url ).toBe(
'https://example.com/wp-admin/admin-ajax.php'
);
} );
} );
} );
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent edge case testing! 👏

This test suite demonstrates exceptional attention to detail with comprehensive coverage of edge cases:

  • Missing jQuery gracefully handled
  • Undefined jQuery scenarios
  • Missing wp.ajax methods don't cause crashes
  • Partial initialization states
  • Pre-existing window.wp properties preserved

The 491 lines of tests provide strong confidence in the robustness of the AJAX configuration. Well done!

Comment on lines 260 to +267

// Set up asset loader domain
assetDomain = configuration.assetLoaderDomain ?: DEFAULT_ASSET_DOMAIN

// Initialize asset loader with configured domain
assetLoader = WebViewAssetLoader.Builder()
.setDomain(assetDomain)
.addPathHandler("/assets/", AssetsPathHandler(this.context))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great implementation! The configurable assetLoaderDomain is properly implemented here:

  • Falls back to DEFAULT_ASSET_DOMAIN if not configured
  • Used to initialize the WebViewAssetLoader with the correct domain
  • Properly constructed into the editorUrl

This enables Android apps to configure a domain that their WordPress site allows in CORS policies, which is essential for AJAX functionality to work.

Address PR feedback about potential race condition. The code now checks
if `window.wp.ajax.send` and `window.wp.ajax.post` are functions before
wrapping them. This prevents TypeError when calling the wrapped function
if the original method was undefined during configuration.

Update tests to verify that missing methods remain undefined rather than
being wrapped with an undefined reference.

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants