diff --git a/projects/js-packages/shared-extension-utils/changelog/update-tiled-gallery-skip-photon-domain b/projects/js-packages/shared-extension-utils/changelog/update-tiled-gallery-skip-photon-domain new file mode 100644 index 000000000000..d7dde041c416 --- /dev/null +++ b/projects/js-packages/shared-extension-utils/changelog/update-tiled-gallery-skip-photon-domain @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Add skipPhotonDomain to the typed Jetpack script data flags. diff --git a/projects/js-packages/shared-extension-utils/declarations.d.ts b/projects/js-packages/shared-extension-utils/declarations.d.ts index ccf9467d2d10..421e3c427229 100644 --- a/projects/js-packages/shared-extension-utils/declarations.d.ts +++ b/projects/js-packages/shared-extension-utils/declarations.d.ts @@ -9,6 +9,7 @@ declare module '@automattic/jetpack-script-data' { jetpack?: { flags: { showJetpackBranding: boolean; + skipPhotonDomain: boolean; }; }; } diff --git a/projects/plugins/jetpack/changelog/update-tiled-gallery-skip-photon-domain b/projects/plugins/jetpack/changelog/update-tiled-gallery-skip-photon-domain new file mode 100644 index 000000000000..09c4828c0f0d --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-tiled-gallery-skip-photon-domain @@ -0,0 +1,4 @@ +Significance: patch +Type: bugfix + +Tiled Gallery: fix images being routed through the external Photon domain on VIP sites, keep existing galleries valid when that changes, and add a `jetpack_skip_photon_domain` filter to control it. diff --git a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/index.js b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/index.js index d58e782338b0..5118a027d4ee 100644 --- a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/index.js +++ b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/index.js @@ -6,8 +6,10 @@ import * as deprecatedV5 from './v5'; import * as deprecatedV6 from './v6'; import * as deprecatedV7 from './v7'; import * as deprecatedV8 from './v8'; +import * as deprecatedV9 from './v9'; export default [ + deprecatedV9, deprecatedV8, deprecatedV7, deprecatedV6, diff --git a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v2/utils/index.js b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v2/utils/index.js index 8fadc3c4ea5f..4b31e500d1d6 100644 --- a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v2/utils/index.js +++ b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v2/utils/index.js @@ -38,8 +38,10 @@ export function photonizedImgProps( img, galleryAtts = {} ) { const { height, width } = img; const { layoutStyle } = galleryAtts; - const photonImplementation = - isWpcomFilesUrl( url ) || true === isVIP() ? photonWpcomImage : photon; + // Deprecated versions have to keep producing the URLs they originally saved, or content saved + // under them stops validating, so they always use the external Photon domain — regardless of what + // the site's Photon-domain setting says. Only the current version follows that setting. + const photonImplementation = isWpcomFilesUrl( url ) ? photonWpcomImage : photon; /** * Build the `src` @@ -97,12 +99,6 @@ export function photonizedImgProps( img, galleryAtts = {} ) { return Object.assign( { src }, srcSet && { srcSet } ); } -function isVIP() { - /*global jetpack_plan*/ - if ( typeof jetpack_plan !== 'undefined' && jetpack_plan.data === 'vip' ) { - return true; - } -} function isWpcomFilesUrl( url ) { const { host } = new URL( url, window.location.href ); return /\.files\.wordpress\.com$/.test( host ); diff --git a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v3/utils/index.js b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v3/utils/index.js index 327c61d005d8..e63b5aeaff34 100644 --- a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v3/utils/index.js +++ b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v3/utils/index.js @@ -39,8 +39,10 @@ export function photonizedImgProps( img, galleryAtts = {} ) { const { height, width } = img; const { layoutStyle } = galleryAtts; - const photonImplementation = - isWpcomFilesUrl( url ) || true === isVIP() ? photonWpcomImage : photon; + // Deprecated versions have to keep producing the URLs they originally saved, or content saved + // under them stops validating, so they always use the external Photon domain — regardless of what + // the site's Photon-domain setting says. Only the current version follows that setting. + const photonImplementation = isWpcomFilesUrl( url ) ? photonWpcomImage : photon; /** * Build the `src` @@ -98,12 +100,6 @@ export function photonizedImgProps( img, galleryAtts = {} ) { return Object.assign( { src }, srcSet && { srcSet } ); } -function isVIP() { - /*global jetpack_plan*/ - if ( typeof jetpack_plan !== 'undefined' && jetpack_plan.data === 'vip' ) { - return true; - } -} function isWpcomFilesUrl( url ) { const { host } = new URL( url, window.location.href ); return /\.files\.wordpress\.com$/.test( host ); diff --git a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v4/utils/index.js b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v4/utils/index.js index 77240e1999be..6abfb95b71f0 100644 --- a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v4/utils/index.js +++ b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v4/utils/index.js @@ -47,8 +47,10 @@ export function photonizedImgProps( img, galleryAtts = {} ) { const { height, width } = img; const { layoutStyle } = galleryAtts; - const photonImplementation = - isWpcomFilesUrl( url ) || true === isVIP() ? photonWpcomImage : photon; + // Deprecated versions have to keep producing the URLs they originally saved, or content saved + // under them stops validating, so they always use the external Photon domain — regardless of what + // the site's Photon-domain setting says. Only the current version follows that setting. + const photonImplementation = isWpcomFilesUrl( url ) ? photonWpcomImage : photon; /** * Build the `src` @@ -106,12 +108,6 @@ export function photonizedImgProps( img, galleryAtts = {} ) { return Object.assign( { src }, srcSet && { srcSet } ); } -function isVIP() { - /*global jetpack_plan*/ - if ( typeof jetpack_plan !== 'undefined' && jetpack_plan.data === 'vip' ) { - return true; - } -} function isWpcomFilesUrl( url ) { const { host } = new URL( url, window.location.href ); return /\.files\.wordpress\.com$/.test( host ); diff --git a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v6/utils/index.js b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v6/utils/index.js index 77240e1999be..6abfb95b71f0 100644 --- a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v6/utils/index.js +++ b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v6/utils/index.js @@ -47,8 +47,10 @@ export function photonizedImgProps( img, galleryAtts = {} ) { const { height, width } = img; const { layoutStyle } = galleryAtts; - const photonImplementation = - isWpcomFilesUrl( url ) || true === isVIP() ? photonWpcomImage : photon; + // Deprecated versions have to keep producing the URLs they originally saved, or content saved + // under them stops validating, so they always use the external Photon domain — regardless of what + // the site's Photon-domain setting says. Only the current version follows that setting. + const photonImplementation = isWpcomFilesUrl( url ) ? photonWpcomImage : photon; /** * Build the `src` @@ -106,12 +108,6 @@ export function photonizedImgProps( img, galleryAtts = {} ) { return Object.assign( { src }, srcSet && { srcSet } ); } -function isVIP() { - /*global jetpack_plan*/ - if ( typeof jetpack_plan !== 'undefined' && jetpack_plan.data === 'vip' ) { - return true; - } -} function isWpcomFilesUrl( url ) { const { host } = new URL( url, window.location.href ); return /\.files\.wordpress\.com$/.test( host ); diff --git a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v8/layout/index.jsx b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v8/layout/index.jsx index 4ea01568d389..fde24346e023 100644 --- a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v8/layout/index.jsx +++ b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v8/layout/index.jsx @@ -37,7 +37,9 @@ export default class Layout extends Component { ); const Image = GalleryImageSave; - const { src, srcSet } = photonizedImgProps( img, { layoutStyle } ); + // This deprecated version has to keep producing the URLs it originally saved, so it always + // uses the external Photon domain. Only the current version follows the site's setting. + const { src, srcSet } = photonizedImgProps( img, { layoutStyle }, { skipPhotonDomain: false } ); return ( + + + ); +} diff --git a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v9/supports.js b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v9/supports.js new file mode 100644 index 000000000000..913b91d0ec4b --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/deprecated/v9/supports.js @@ -0,0 +1,15 @@ +// Frozen snapshot of the supports at the time this markup was current. See ./attributes.js. +export default { + align: [ 'center', 'wide', 'full' ], + color: { + gradients: true, + text: false, + }, + customClassName: false, + html: false, + spacing: { + margin: true, + padding: true, + }, + __experimentalHideChildBlockControls: true, +}; diff --git a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/layout/index.jsx b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/layout/index.jsx index 837a377998cf..652024fe0de7 100644 --- a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/layout/index.jsx +++ b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/layout/index.jsx @@ -28,6 +28,7 @@ export default class Layout extends Component { onSelectImage, selectedImage, setImageAttributes, + skipPhotonDomain, } = this.props; const ariaLabel = sprintf( @@ -38,7 +39,9 @@ export default class Layout extends Component { ); const Image = isSave ? GalleryImageSave : GalleryImageEdit; - const { src, srcSet } = photonizedImgProps( img, { layoutStyle } ); + // Only the deprecated version that has to keep emitting the external Photon domain passes this; + // left undefined, the images follow whatever this site asks for. + const { src, srcSet } = photonizedImgProps( img, { layoutStyle }, { skipPhotonDomain } ); return ( { + beforeEach( () => { + registerBlockType( metadata.name, { ...metadata, save, deprecated } ); + } ); + + afterEach( () => { + unregisterBlockType( metadata.name ); + delete window.JetpackScriptData; + } ); + + it( 'saves images on the external Photon domain by default', () => { + setSkipPhotonDomain( false ); + + expect( saveGallery() ).toContain( 'https://i0.wp.com/example.com/wp-content/uploads' ); + } ); + + it( 'saves images on the site host when the site skips the Photon domain', () => { + setSkipPhotonDomain( true ); + const markup = saveGallery(); + + expect( markup ).not.toContain( 'i0.wp.com' ); + expect( markup ).toContain( 'src="https://example.com/wp-content/uploads/2026/07/one.png"' ); + } ); + + // The regression this guards: a VIP site's galleries were saved with Photon-domain URLs before the + // site's setting was honoured, and every one of them was flagged invalid on the next editor load. + it( 'keeps galleries saved on the Photon domain valid once the site skips it', () => { + setSkipPhotonDomain( false ); + const markupSavedBefore = saveGallery(); + + setSkipPhotonDomain( true ); + const [ block ] = parse( markupSavedBefore ); + + expect( block.name ).toBe( metadata.name ); + expect( block.isValid ).toBe( true ); + expect( block.attributes.images ).toHaveLength( 2 ); + // Gutenberg informs when a deprecation matched, so this also proves it was rescued rather than + // matching the current save() by accident. + expect( console ).toHaveInformed(); + } ); + + it( 're-serializes those galleries onto the site host', () => { + setSkipPhotonDomain( false ); + const markupSavedBefore = saveGallery(); + + setSkipPhotonDomain( true ); + const reSerialized = serialize( parse( markupSavedBefore ) ); + + expect( reSerialized ).not.toContain( 'i0.wp.com' ); + expect( console ).toHaveInformed(); + } ); + + it( 'keeps galleries saved on the site host valid while the site still skips the domain', () => { + setSkipPhotonDomain( true ); + const [ block ] = parse( saveGallery() ); + + expect( block.isValid ).toBe( true ); + } ); + + // Galleries using custom links match the deprecation added for this change and nothing else — the + // much older v6 covers plain galleries by whitespace luck, but knows nothing about custom links. + // This case is what keeps that deprecation from being deleted as redundant. + it( 'keeps galleries with custom links valid too', () => { + setSkipPhotonDomain( false ); + const markupSavedBefore = serialize( + createBlock( metadata.name, { + images: IMAGES.map( image => ( { ...image, customLink: `${ image.link }custom/` } ) ), + ids: [ 5, 6 ], + linkTo: 'custom', + } ) + ); + + setSkipPhotonDomain( true ); + const [ block ] = parse( markupSavedBefore ); + + expect( block.isValid ).toBe( true ); + expect( block.attributes.images[ 0 ].customLink ).toBe( 'https://example.com/one/custom/' ); + expect( console ).toHaveInformed(); + } ); +} ); diff --git a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/tiled-gallery.php b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/tiled-gallery.php index 850d417a4958..54e2d01586bd 100644 --- a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/tiled-gallery.php +++ b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/tiled-gallery.php @@ -11,7 +11,6 @@ namespace Automattic\Jetpack\Extensions; use Automattic\Jetpack\Blocks; -use Automattic\Jetpack\Current_Plan as Jetpack_Plan; use Automattic\Jetpack\Status; use Automattic\Jetpack\Status\Host; use Jetpack; @@ -63,10 +62,6 @@ public static function render( $attr, $content ) { Jetpack_Gutenberg::load_assets_as_required( __DIR__ ); $is_squareish_layout = self::is_squareish_layout( $attr ); - // For backward compatibility (ensuring Tiled Galleries using now deprecated versions of the block are not affected). - // See isVIP() in utils/index.js. - $jetpack_plan = Jetpack_Plan::get(); - wp_localize_script( 'jetpack-gallery-settings', 'jetpack_plan', array( 'data' => $jetpack_plan['product_slug'] ) ); if ( preg_match_all( '/]+>/', $content, $images ) ) { /** diff --git a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/utils/index.js b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/utils/index.js index df914545e390..3ed594d2bcbb 100644 --- a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/utils/index.js +++ b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/utils/index.js @@ -1,22 +1,10 @@ -import { isWoASite, isSimpleSite } from '@automattic/jetpack-script-data'; +import { getScriptData, isWoASite, isSimpleSite } from '@automattic/jetpack-script-data'; import { isPrivateSite } from '@automattic/jetpack-shared-extension-utils'; import { isBlobURL } from '@wordpress/blob'; -import { select } from '@wordpress/data'; import photon from 'photon'; import isOfflineMode from '../../../shared/is-offline-mode'; import { PHOTON_MAX_RESIZE } from '../constants'; -let jetpackPlanFromState; - -window.addEventListener( 'load', function () { - const hasImageCompare = select( 'core/block-editor' ) - .getBlocks() - .some( block => block.name === 'jetpack/image-compare' ); - if ( hasImageCompare && ! jetpackPlanFromState ) { - jetpackPlanFromState = window?.Jetpack_Editor_Initial_State?.jetpack?.jetpack_plan; - } -} ); - export function isSquareishLayout( layout ) { return [ 'circle', 'square' ].includes( layout ); } @@ -24,16 +12,24 @@ export function isSquareishLayout( layout ) { /** * Build src and srcSet properties which can be used on an * - * @param {object} img - Image - * @param {number} img.height - Image height - * @param {string} img.url - Image URL - * @param {number} img.width - Image width - * @param {object} galleryAtts - Gallery attributes relevant for image optimization. - * @param {string} galleryAtts.layoutStyle - Gallery layout. 'rectangular', 'circle', etc. - * @param {number} galleryAtts.columns - Gallery columns. Not applicable for all layouts. + * @param {object} img - Image + * @param {number} img.height - Image height + * @param {string} img.url - Image URL + * @param {number} img.width - Image width + * @param {object} galleryAtts - Gallery attributes relevant for image optimization. + * @param {string} galleryAtts.layoutStyle - Gallery layout. 'rectangular', 'circle', etc. + * @param {number} galleryAtts.columns - Gallery columns. Not applicable for all layouts. + * @param {object} options - Options. + * @param {boolean} options.skipPhotonDomain - Whether to skip the external Photon domain. Defaults + * to what the site asks for; deprecated block versions + * pass `false` to keep emitting the URLs they saved. * @return {object} Returns an object. If possible, the object will include `src` and `srcSet` properties {string} for use on an image. */ -export function photonizedImgProps( img, galleryAtts = {} ) { +export function photonizedImgProps( + img, + galleryAtts = {}, + { skipPhotonDomain: skipDomain = skipPhotonDomain() } = {} +) { if ( ! img.height || ! img.width ) { return img.url ? { src: img.url } : {}; } @@ -59,7 +55,7 @@ export function photonizedImgProps( img, galleryAtts = {} ) { const { height, width } = img; const { layoutStyle } = galleryAtts; - const photonImplementation = true === isVIP() || isSimpleSite() ? photonWpcomImage : photon; + const photonImplementation = skipDomain || isSimpleSite() ? photonWpcomImage : photon; /** * Build the `src` @@ -117,16 +113,21 @@ export function photonizedImgProps( img, galleryAtts = {} ) { return Object.assign( { src }, srcSet && { srcSet } ); } -function isVIP() { - /*global jetpack_plan*/ - // Use `jetpackPlanFromState` if available, otherwise fall back to `jetpack_plan` defined within the render function in tiled-gallery.php. - let jetpackPlan; - if ( typeof jetpackPlanFromState !== 'undefined' ) { - jetpackPlan = jetpackPlanFromState; - } else if ( typeof jetpack_plan !== 'undefined' ) { - jetpackPlan = jetpack_plan; - } - return jetpackPlan && jetpackPlan?.data === 'vip'; +/** + * Whether the current site should skip the external Photon (photon.js) domain and instead build + * files.wordpress.com-style URLs via photonWpcomImage. + * + * The value is computed in PHP (true on VIP sites, overridable via the `jetpack_skip_photon_domain` + * filter) and delivered in the script data. It is deliberately read from that global rather than from + * editor settings or any other store: this runs inside the blocks' save() output, which is + * regenerated during block validation while the post is parsed — before the editor stores hold any + * settings. Reading store state there yields the default value on the first pass and marks saved + * galleries as invalid. + * + * @return {boolean} True when the external Photon domain should be skipped. + */ +export function skipPhotonDomain() { + return true === getScriptData()?.jetpack?.flags?.skipPhotonDomain; } /** diff --git a/projects/plugins/jetpack/extensions/blocks/tiled-gallery/utils/test/index.js b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/utils/test/index.js new file mode 100644 index 000000000000..75e0178add45 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/tiled-gallery/utils/test/index.js @@ -0,0 +1,79 @@ +import { photonizedImgProps, skipPhotonDomain } from '..'; + +const IMAGE = { + url: 'https://example.com/wp-content/uploads/2026/01/cat.jpg', + width: 1200, + height: 800, +}; + +/** + * Set the script data global, the way the PHP inline script does on a page load. + * + * @param {object} jetpack - The `jetpack` section of the script data. + */ +function setScriptData( jetpack = {} ) { + window.JetpackScriptData = { jetpack }; +} + +describe( 'skipPhotonDomain', () => { + afterEach( () => { + delete window.JetpackScriptData; + } ); + + it( 'is false when the script data is missing', () => { + expect( skipPhotonDomain() ).toBe( false ); + } ); + + it( 'is false when the script data does not flag it', () => { + setScriptData( { flags: { showJetpackBranding: true } } ); + + expect( skipPhotonDomain() ).toBe( false ); + } ); + + // The value has to come from this global rather than an editor store: it feeds the blocks' save() + // output, which is regenerated during block validation before any store holds editor settings. + it( 'is true when the script data flags it', () => { + setScriptData( { flags: { skipPhotonDomain: true } } ); + + expect( skipPhotonDomain() ).toBe( true ); + } ); +} ); + +describe( 'photonizedImgProps', () => { + afterEach( () => { + delete window.JetpackScriptData; + } ); + + it( 'routes images through the Photon domain by default', () => { + const { src, srcSet } = photonizedImgProps( IMAGE ); + + expect( src ).toBe( 'https://i0.wp.com/example.com/wp-content/uploads/2026/01/cat.jpg?ssl=1' ); + expect( srcSet ).toContain( + 'https://i0.wp.com/example.com/wp-content/uploads/2026/01/cat.jpg' + ); + } ); + + it( 'keeps images on the site host when the Photon domain is skipped', () => { + setScriptData( { flags: { skipPhotonDomain: true } } ); + + const { src, srcSet } = photonizedImgProps( IMAGE ); + + expect( src ).toBe( 'https://example.com/wp-content/uploads/2026/01/cat.jpg' ); + expect( srcSet ).not.toContain( 'i0.wp.com' ); + expect( srcSet ).toContain( + 'https://example.com/wp-content/uploads/2026/01/cat.jpg?strip=info&w=600' + ); + } ); + + // Deprecated block versions pass this so they keep emitting the URLs they originally saved. + it( 'stays on the Photon domain when the caller overrides the site setting', () => { + setScriptData( { flags: { skipPhotonDomain: true } } ); + + const { src, srcSet } = photonizedImgProps( IMAGE, {}, { skipPhotonDomain: false } ); + + expect( src ).toBe( 'https://i0.wp.com/example.com/wp-content/uploads/2026/01/cat.jpg?ssl=1' ); + expect( srcSet ).toContain( + 'https://i0.wp.com/example.com/wp-content/uploads/2026/01/cat.jpg' + ); + } ); +} ); diff --git a/projects/plugins/jetpack/src/class-jetpack-script-data.php b/projects/plugins/jetpack/src/class-jetpack-script-data.php index f57935611918..1443efb4af42 100644 --- a/projects/plugins/jetpack/src/class-jetpack-script-data.php +++ b/projects/plugins/jetpack/src/class-jetpack-script-data.php @@ -9,6 +9,8 @@ namespace Automattic\Jetpack\Plugin; +use Automattic\Jetpack\Current_Plan as Jetpack_Plan; + /** * Jetpack_Script_Data class. */ @@ -40,9 +42,51 @@ public static function set_admin_script_data( $data ) { $data['jetpack'] = array( 'flags' => array( 'showJetpackBranding' => (bool) apply_filters( 'jetpack_show_editor_panel_branding', true ), + 'skipPhotonDomain' => self::should_skip_photon_domain(), ), ); return $data; } + + /** + * Whether image-serving blocks should skip the external Photon domain (i0.wp.com) and build + * URLs on the site's own Photon-like host instead. + * + * VIP sites serve images from a host of their own, and images routed through the public Photon + * domain are not reachable there. + * + * This is read by the Tiled Gallery block's skipPhotonDomain(), which runs inside the block's + * save() output — including when the editor regenerates that output to validate saved content. + * That regeneration happens while the post is being parsed, before any editor store holds + * settings, so the value has to travel in the script data rather than in editor settings. + * + * The signal is the plan slug rather than Host::is_vip_site(): it is what the block's previous + * isVIP() check used, and switching to the WPCOM_IS_VIP_ENV constant would change which sites get + * site-host URLs — and so which existing galleries need a deprecation to stay valid. + * + * @since $$next-version$$ + * + * @return bool + */ + private static function should_skip_photon_domain() { + $jetpack_plan = Jetpack_Plan::get(); + + /** + * Filter whether the Tiled Gallery and Image Compare blocks should skip the external + * Photon (i0.wp.com) domain and build image URLs on the site's own host instead. + * + * Defaults to true on VIP sites only, and false everywhere else. + * + * Changing this changes the markup those blocks save, so existing galleries are re-serialized + * the next time they are saved. + * + * @module tiled-gallery + * + * @since $$next-version$$ + * + * @param bool $skip_photon_domain Whether to skip the external Photon domain. + */ + return (bool) apply_filters( 'jetpack_skip_photon_domain', 'vip' === $jetpack_plan['product_slug'] ); + } } diff --git a/projects/plugins/jetpack/tests/php/src/Jetpack_Script_Data_Test.php b/projects/plugins/jetpack/tests/php/src/Jetpack_Script_Data_Test.php index f405bfea9fe0..11fe197f74d6 100644 --- a/projects/plugins/jetpack/tests/php/src/Jetpack_Script_Data_Test.php +++ b/projects/plugins/jetpack/tests/php/src/Jetpack_Script_Data_Test.php @@ -5,6 +5,7 @@ * @package automattic/jetpack */ +use Automattic\Jetpack\Current_Plan as Jetpack_Plan; use Automattic\Jetpack\Plugin\Jetpack_Script_Data; use PHPUnit\Framework\Attributes\CoversClass; @@ -17,15 +18,53 @@ class Jetpack_Script_Data_Test extends WP_UnitTestCase { use \Automattic\Jetpack\PHPUnit\WP_UnitTestCase_Fix; + /** + * Test set up. + */ + public function set_up() { + parent::set_up(); + $this->reset_active_plan_cache(); + } + /** * Test tear down. */ public function tear_down() { remove_all_filters( 'jetpack_show_editor_panel_branding' ); remove_all_filters( 'jetpack_admin_js_script_data' ); + remove_all_filters( 'jetpack_skip_photon_domain' ); + remove_all_filters( 'pre_option_jetpack_active_plan' ); + $this->reset_active_plan_cache(); parent::tear_down(); } + /** + * Drop Current_Plan's per-request cache so each test sees the plan it sets up. + */ + private function reset_active_plan_cache() { + $prop = new ReflectionProperty( Jetpack_Plan::class, 'active_plan_cache' ); + // setAccessible() is a no-op (and deprecated) since PHP 8.1; only needed for older versions. + if ( PHP_VERSION_ID < 80100 ) { + $prop->setAccessible( true ); + } + $prop->setValue( null, null ); + } + + /** + * Pretend the site is on the given plan, the way a synced plan would look. + * + * @param string $product_slug Plan product slug. + */ + private function set_plan( $product_slug ) { + add_filter( + 'pre_option_jetpack_active_plan', + function () use ( $product_slug ) { + return array( 'product_slug' => $product_slug ); + } + ); + $this->reset_active_plan_cache(); + } + /** * Tests that configure() registers the filter. */ @@ -51,6 +90,34 @@ public function test_filter_disables_branding() { $this->assertFalse( $result['jetpack']['flags']['showJetpackBranding'] ); } + /** + * Tests that sites keep using the external Photon domain by default. + */ + public function test_default_skip_photon_domain_is_false() { + $this->set_plan( 'jetpack_free' ); + $result = Jetpack_Script_Data::set_admin_script_data( array() ); + $this->assertFalse( $result['jetpack']['flags']['skipPhotonDomain'] ); + } + + /** + * Tests that VIP sites skip the external Photon domain. + */ + public function test_skip_photon_domain_is_true_on_vip() { + $this->set_plan( 'vip' ); + $result = Jetpack_Script_Data::set_admin_script_data( array() ); + $this->assertTrue( $result['jetpack']['flags']['skipPhotonDomain'] ); + } + + /** + * Tests that the filter can flip the decision on a non-VIP site. + */ + public function test_filter_overrides_skip_photon_domain() { + $this->set_plan( 'jetpack_free' ); + add_filter( 'jetpack_skip_photon_domain', '__return_true' ); + $result = Jetpack_Script_Data::set_admin_script_data( array() ); + $this->assertTrue( $result['jetpack']['flags']['skipPhotonDomain'] ); + } + /** * Tests that existing data in the array is preserved. */