Repository navigation
feat: AJAX uses token authentication - #181
Conversation
a051ea4 to
682e0df
Compare
686ebf7 to
bbaf9c4
Compare
This comment was marked as outdated.
This comment was marked as outdated.
394bce2 to
ea73b3c
Compare
e2ead31 to
80aca45
Compare
Include authorization header in AJAX requets, as we do not have cookies to send in the mobile app environment.
If we configure AJAX before loading the library, the configuration is overridden.
This global is often used by WordPress Admin page scripts.
Useful when needing to allow CORS for specific domains.
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>
When `videopress/video` is not in `allowed_block_types`, initialize the VideoPress AJAX bridge to handle `core/video` blocks extended to rely upon VideoPress upload services. AJAX auth is always initialized. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the iOS and Android code examples out of the Android-specific requirement so they are not visually nested under that bullet point. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
configureAjax() now initializes wp.ajax, wp.ajax.settings, and the AJAX URL before the VideoPress bridge runs, making the duplicate setup in initializeVideoPressAjaxBridge() unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wrap `new URL(siteURL)` in try/catch so a malformed siteURL logs a warning instead of throwing. - Guard `configureMediaAjax` against missing `wp.ajax.send`/`post` (e.g., if wp-util.js failed to load). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Throw IllegalArgumentException if the value contains a scheme, path, or is blank, so callers get a clear error instead of a malformed asset URL at runtime. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Record the upstream commit hash and rationale for vendoring so future maintainers know where the file came from and when to update it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Account.WpCom.username stores just the hostname (e.g., "dcpaid.wordpress.com") since it is extracted via URI.host during OAuth. ConfigurationItem was using this bare hostname as siteUrl, producing invalid AJAX endpoints. Prepend "https://" to match the self-hosted flow, which receives a full URL from the callback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
c09e90a to
282c499
Compare
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| return EditorConfigurationBuilder( | ||
| postType: selectedPostTypeDetails, | ||
| siteURL: URL(string: apiRoot.siteUrlString())!, | ||
| siteURL: URL(string: apiRoot.homeUrlString())!, |
There was a problem hiding this comment.
Ensures WP.com sites use TLS for the siteURL configuration in the demo app.
| if ( ! allowedBlockTypes?.includes( 'videopress/video' ) ) { | ||
| // The VideoPress block isn't available, so initialize the bridge to handle | ||
| // any `core/video` blocks extended to rely upon VideoPress upload services. | ||
| initializeVideoPressAjaxBridge(); | ||
| } |
There was a problem hiding this comment.
Retained to continue support for WP.com core/video blocks that are extended to rely upon VideoPress upload services. This can be removed in the future once videopress/video support is enabled and deemed stable.
| accountId = account.id, | ||
| name = account.username, | ||
| siteUrl = account.username, | ||
| siteUrl = "https://${account.username}", |
There was a problem hiding this comment.
The account.username is protocol-less, which causes errors as a GBK.siteURL value. It is safe to assume TLS for WP.com sites.
| // Initialize wp.ajax if not already present | ||
| window.wp.ajax = window.wp.ajax || {}; | ||
| window.wp.ajax.settings = window.wp.ajax.settings || {}; | ||
|
|
||
| // Set up AJAX settings with site URL | ||
| const { siteURL } = getGBKit(); | ||
| if ( siteURL ) { | ||
| window.wp.ajax.settings.url = `${ siteURL }/wp-admin/admin-ajax.php`; | ||
| } |
There was a problem hiding this comment.
Now redundant of the AJAX-specific configuration that runs before this bridge.
Derive the WebViewAssetLoader domain from the configured siteURL instead of defaulting to the synthetic appassets.androidplatform.net domain. This makes REST API and admin-ajax.php requests same-origin, eliminating CORS restrictions without requiring server-side headers. - Restrict shouldOverrideUrlLoading to /assets/ paths on the asset domain so arbitrary site pages don't load inside the WebView. - Reorder shouldInterceptRequest to check the cache interceptor before the asset loader, preventing cached JS/CSS from being short-circuited when both share the site domain. - Remove the now-unnecessary assetLoaderDomain configuration option from EditorConfiguration. - Update AJAX documentation to reflect the simplified setup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@dcalhoun This is looking good, but I'm not sure about "Step 8: Verify the request relies upon the REST API, not admin-ajax." What exactly should I be looking for? |
@nbradbury it references using Chrome to inspect the Network activity of the mobile device/emulator. When inspecting, the Network tab should show requests. Uploading a Video should result in Below is a screenshot of example
|
kean
left a comment
There was a problem hiding this comment.
I tested the changes and verified that the "Authorization" header was present and that the existing upload media functionality is working. The code looks good.
The comment justified `*` partly by claiming the editor loads from file:// (Origin null) and so can't be allowlisted. That holds on iOS, but the Android editor has loaded from the site's own origin since #181, where echoing the origin would be perfectly possible. Name the document, not the origin, as what scopes the token: same-origin site pages exist on Android, and it is the per-document JS global that keeps them from reading it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh
`onPageStarted` received the loaded URL and discarded it, so any page reaching the main frame was handed `window.GBKit` — the site credential and the upload server's port and token. `shouldOverrideUrlLoading` admits several site URLs into that frame, and since #181 the editor shares an origin with the site, so those pages are served by the site's own theme and plugins. Check the destination before advertising the globals or starting the upload server, matching the dev server by authority so a local site on another port of the same host is not mistaken for the editor. Readiness still resets for any page, since navigating away from the editor leaves it unusable either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh
`onPageStarted` received the loaded URL and discarded it, so any page reaching the main frame was handed `window.GBKit` — the site credential and the upload server's port and token. `shouldOverrideUrlLoading` admits several site URLs into that frame, and since #181 the editor shares an origin with the site, so those pages are served by the site's own theme and plugins. Check the destination before advertising the globals or starting the upload server, matching the dev server by authority so a local site on another port of the same host is not mistaken for the editor. Readiness still resets for any page, since navigating away from the editor leaves it unusable either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh
The comment justified `*` partly by claiming the editor loads from file:// (Origin null) and so can't be allowlisted. That holds on iOS, but the Android editor has loaded from the site's own origin since #181, where echoing the origin would be perfectly possible. Name the document, not the origin, as what scopes the token: same-origin site pages exist on Android, and it is the per-document JS global that keeps them from reading it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh
The comment justified `*` partly by claiming the editor loads from file:// (Origin null) and so can't be allowlisted. That holds on iOS, but the Android editor has loaded from the site's own origin since #181, where echoing the origin would be perfectly possible. Name the document, not the origin, as what scopes the token: same-origin site pages exist on Android, and it is the per-document JS global that keeps them from reading it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh
The comment justified `*` partly by claiming the editor loads from file:// (Origin null) and so can't be allowlisted. That holds on iOS, but the Android editor has loaded from the site's own origin since #181, where echoing the origin would be perfectly possible. Name the document, not the origin, as what scopes the token: same-origin site pages exist on Android, and it is the per-document JS global that keeps them from reading it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh
… editor frame (#730) * fix(android): inject the editor globals into the editor document only `onPageStarted` received the loaded URL and discarded it, so any page reaching the main frame was handed `window.GBKit` — the site credential and the upload server's port and token. `shouldOverrideUrlLoading` admits several site URLs into that frame, and since #181 the editor shares an origin with the site, so those pages are served by the site's own theme and plugins. Check the destination before advertising the globals or starting the upload server, matching the dev server by authority so a local site on another port of the same host is not mistaken for the editor. Readiness still resets for any page, since navigating away from the editor leaves it unusable either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh * fix(android): match the REST API by its configured root, not by substring The navigation policy admitted any site URL whose path contained `/wp-json/` or whose query contained `rest_route=`. Both are satisfied by ordinary pages — `/blog/wp-json/a-post`, or any URL carrying `?utm_campaign=rest_route=x` — which WordPress serves with the site's theme and plugins, inside the editor's own frame. Compare against the configured `siteApiRoot` instead: a path under its path root, or `rest_route` as an actual query parameter. Reading the root also settles the cases the characters cannot, so the same path is the API on a subdirectory install and a page on a root install. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh * fix(android): match a rest_route API root by its query alone Without pretty permalinks the API root is `/index.php?rest_route=/`, and `/index.php` also serves ordinary pages, so matching its path admitted them into the editor frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * fix(android): match the API root path by whole path segments A root without a trailing slash, such as `/wp-json`, otherwise prefixes page slugs like `/wp-json-tutorial/`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * fix(android): ignore an empty rest_route when matching the REST API WordPress skips an empty route, including `0`, and renders the requested page with the site's theme instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * test(android): assert nothing reaches a non-editor page on start Checking only the last evaluated script would pass if another script ran after an injection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * fix(android): match editor assets by the scheme the asset loader serves The asset loader serves one scheme, so the same path over the other reaches the site over the network, yet it was admitted and handed the editor globals. One helper now backs both checks so they can't drift apart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * docs(android): note that only the editor document receives the globals Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * fix(android): match the editor document by its exact asset path The asset loader also serves the host app's other bundled pages, some of which load third-party scripts, and those received the editor globals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * fix(android): let a rest_route parameter decide over the API root path WordPress lets the parameter override the route a `/wp-json/` path sets, so `/wp-json/?rest_route=` serves the themed front page, yet it passed the path check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * fix(android): open REST API navigations in the browser The editor reaches the REST API by fetch, which never passes through `shouldOverrideUrlLoading`, so the allowlist only admitted navigations. Those let site pages whose URLs WordPress reads differently from Android, and http API URLs on https sites, replace the editor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * test(android): explain how a site page still reaches the editor frame Since the REST allowlist was removed, the navigation policy admits no site pages, but loads it never sees still can. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * test(android): assert a non-editor page leaves the upload server down Nothing pinned the editor-only check above the server start, so reordering them would have passed every test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * refactor(android): set the asset scheme before installing the WebView client The client reads it, so assigning it alongside the asset authority avoids relying on no navigation running in between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * docs(android): correct why isAssetUrl matches one scheme A local http site's asset loader serves https too, so the other scheme doesn't always reach the site over the network. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * fix(android): compare origin hosts case-insensitively Chromium lowercases the host it reports, so a dev server URL written with capitals, like a Mac's mDNS name, never matched and the editor received no globals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * refactor(android): recognize the editor document by the URL it loaded Re-deriving the editor URL duplicated loadEditor's choice and let any page on the dev server's host receive the globals. Comparing against the recorded URL keeps the two in step and requires the exact document. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * test(android): cover injection after a reload and on a local http site Neither path had a positive case, so dropping the globals from either would have passed every test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs * test(android): build one view per navigation test Each URL built its own editor view, loading the editor eight times where two views suffice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: read the editor configuration from the injected global only `getGBKit` fell back to a copy of the configuration in `localStorage`. Boot waits for `window.GBKit` before anything reads the configuration, and outside `?dev_mode` aborts when it never arrives, so the fallback could only ever serve a previous session's values to a dev-mode page with no host. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV * fix(ios): stop persisting the editor configuration `GBKit` carries the site credential and the local server's port and tokens, all valid only for the load that injected them, and iOS mirrored it into `localStorage`, which the default website data store keeps on disk across launches. The document-start user script replays the global on every navigation, including the reload after a WebContent process termination, so the copy had no reader. Remove the key as the configuration is injected so devices upgraded from an earlier version are scrubbed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV * fix(android): stop persisting the editor configuration `GBKit` carries the site credential and the local server's port and token, all valid only for the load that injected them. The view re-injects the global on every page start and wipes web storage before each load, so the `localStorage` copy had no reader and nothing left to clear on detach. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV * docs: describe where the local server's token lives The CORS rationale on both platforms named `localStorage` alongside `window.GBKit` as where the editor holds the per-session bearer token. The token now lives in the injected global only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV * docs(android): correct the CORS rationale's origin premise The comment justified `*` partly by claiming the editor loads from file:// (Origin null) and so can't be allowlisted. That holds on iOS, but the Android editor has loaded from the site's own origin since #181, where echoing the origin would be perfectly possible. Name the document, not the origin, as what scopes the token: same-origin site pages exist on Android, and it is the per-document JS global that keeps them from reading it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh * refactor(ios): drop the dead completion value from the configuration script The trailing `"done";` dates from #14, when the configuration was injected with `evaluateJavaScript`, which needed a serializable trailing expression. #15 moved the script to a document-start `WKUserScript`, whose completion value WebKit discards, and the line has been inert since. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh * test: cover getGBKit's injected-global-only contract `getGBKit` now ignores any persisted copy, but nothing held that contract in place: the suite only ever exercised it through `window.GBKit`, so restoring the storage fallback would have left every test green. Verified as a tripwire — reinstating the fallback fails the second case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh * docs: say when the configuration scrub runs The iOS comment promised that an upgraded device "is scrubbed" without saying this only happens on the next editor load, and left no signal for when the migration can be dropped. Document `clearConfig` as teardown-only: with the storage fallback gone, `window.GBKit` is the editor's only source of configuration, so clearing it under a live editor now leaves that editor unusable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh * docs: rest the CORS rationale on the token alone Both comments claimed no other document can read the editor's global, which overstated the guarantee and disagreed across platforms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEok5vK3cCWGfcQp8zEGWW --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

Related:
@wordpress/api-fetchutility for GutenbergKit only Automattic/jetpack#45254What?
Authenticate AJAX requests with application passwords sent via an authorization header. Vendor
wp-util.jsand configure the WordPress AJAX and media globals.Why?
The GutenbergKit editor does not have authorization cookies, so we must rely upon a different authorization mechanism. Additionally, GutenbergKit excludes core WordPress assets from the editor assets endpoint, so
wp-util.js(which provideswp.ajaxandwp.template) must be vendored and loaded directly.Ref CMM-713. Close CMM-768.
How?
Authorizationheader viajQuery.ajaxPrefilterand by overloading thewindow.wp.ajaxutilities. This general-purpose AJAX auth is always initialized.wp-util.jsafter jQuery and lodash are onwindow, since its IIFE captures jQuery via closure at execution time.lodash-js-afterinline script from editor assets. WordPress's_.noConflict()call wipeswindow._because GutenbergKit doesn't load Underscore.js.wp.media.ajaxandwp.media.postto the authenticatedwp.ajaxmethods, since WordPress core'smedia-models.jsis not loaded.videopress/videois not inallowed_block_types, socore/videoblocks extended to rely upon VideoPress upload services continue to work.WebViewAssetLoaderdomain fromsiteURLso that the editor document shares the site's origin, making REST API andadmin-ajax.phprequests same-origin and eliminating CORS restrictions without server-side headers.homeURL from the REST API response instead ofurl, which returnshttp://for WordPress.com sites.Testing Instructions
1. Verify AJAX requests use token authentication
make buildchrome://inspect) device.Authorizationheader with the token.2. Verify VideoPress bridge continues functioning
make buildchrome://inspect) device.Accessibility Testing Instructions
N/A, no navigation changes.
Screenshots or screencast
N/A, no visual changes.