Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'm not sure if this is the right place for this file; maybe it should be in a higher directory to cover all plugin files?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can probably define this into /.gitignore or just leave it in the project-specific area since it doesn't matter all that much.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I have no strong opinion about where this file should be located 😄 Please feel free to move it elsewhere if necessary.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
package-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php return array('dependencies' => array('react-jsx-runtime', 'wp-blob', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => 'f13102762651a2180e59');

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "wporg-media-resource-checker",
"version": "1.0.0",
"description": "Displays warnings in the block editor when media resources are from domains other than the recommended ones.",
"author": "WordPress.org",
"license": "GPL-2.0-or-later",
"scripts": {
"start": "wp-scripts start",
"build": "wp-scripts build",
"format": "wp-scripts format src",
"packages-update": "wp-scripts packages-update"
},
"devDependencies": {
"@wordpress/icons": "^11.3.0",
"@wordpress/scripts": "^31.1.0"
},
"dependencies": {
"clsx": "^2.1.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';

/**
* Internal dependencies
*/
import { getBlockMediaResourceToCheck, isInvalidResource } from './utils';

/**
* Custom hook to check if a block has an invalid media resource.
*
* @param {string} name The block name.
* @param {Object} attributes The block attributes.
* @return {Object} Object containing hasInvalidResource, siteUrl, and mediaUrl.
*/
export const useHasInvalidSource = ( name, attributes ) => {
const siteUrl = useSelect( ( select ) => {
const siteData = select( coreStore ).getEntityRecord(
'root',
'__unstableBase'
);
return siteData?.home || siteData?.url || null;
}, [] );

const mediaUrl = getBlockMediaResourceToCheck( name, attributes );

const hasInvalidResource =
mediaUrl && isInvalidResource( mediaUrl, siteUrl );

return {
hasInvalidResource,
siteUrl,
mediaUrl,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/**
* External dependencies
*/
import clsx from 'clsx';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { addFilter } from '@wordpress/hooks';
import { createHigherOrderComponent } from '@wordpress/compose';
import { BlockControls } from '@wordpress/block-editor';
import {
Dropdown,
ExternalLink,
ToolbarButton,
ToolbarGroup,
} from '@wordpress/components';
import { caution } from '@wordpress/icons';
import { getAuthority } from '@wordpress/url';

/**
* Internal dependencies
*/
import { ALLOWED_DOMAINS } from './utils';
import { useHasInvalidSource } from './hooks';
import './style.scss';

/**
* Block edit component with warning.
*
* @param {Object} props Component props.
* @param {Object} props.BlockEdit The block edit component.
* @return {Function} The block edit with warning component.
*/
const BlockEditWithWarning = ( { BlockEdit, siteUrl, mediaUrl, ...props } ) => {
const siteAuthority = getAuthority( siteUrl );
const allowedDomainList = [
siteAuthority,
...ALLOWED_DOMAINS.map( ( domain ) => domain.authority ),
];

return (
<>
<BlockEdit { ...props } />
{ props.isSelected && (
<BlockControls>
<Dropdown
contentClassName=""
renderToggle={ ( { isOpen, onToggle } ) => {
return (
<ToolbarGroup>
<ToolbarButton
aria-expanded={ isOpen }
aria-haspopup="true"
onClick={ onToggle }
label={ __( 'Media resource error' ) }
icon={ caution }
className="wporg-media-resource-checker-toolbar-button"
/>
</ToolbarGroup>
);
} }
renderContent={ () => {
return (
<div className="wporg-media-resource-checker-popover-content">
<p>
{ __(
'This media resource is from a domain other than the recommended ones.',
'wporg-media-resource-checker'
) }
</p>
<p>
{ mediaUrl && (
<ExternalLink href={ mediaUrl }>
{ mediaUrl }
</ExternalLink>
) }
</p>
<p>
{ __(
'Please use a media resource from the following recommended domains:',
'wporg-media-resource-checker'
) }
</p>
<ul>
{ allowedDomainList.map( ( domain ) => (
<li key={ domain }>{ domain }</li>
) ) }
</ul>
</div>
);
} }
/>
</BlockControls>
) }
</>
);
};

/**
* Higher order component to check if the media resource is from a domain
* other than the recommended ones.
*
* @param {Function} BlockEdit The block edit component.
* @return {Function} The higher order component.
*/
const withMediaResourceChecker = createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => {
const { name, attributes } = props;

const { hasInvalidResource, siteUrl, mediaUrl } = useHasInvalidSource(
name,
attributes
);

return hasInvalidResource ? (
<BlockEditWithWarning
BlockEdit={ BlockEdit }
siteUrl={ siteUrl }
mediaUrl={ mediaUrl }
{ ...props }
/>
) : (
<BlockEdit key="edit" { ...props } />
);
};
}, 'withMediaResourceChecker' );

/**
* Higher order component to add className to wrapperProps for blocks
* with invalid resources.
*
* @param {Function} BlockListBlock The block list block component.
* @return {Function} The higher order component.
*/
const withInvalidResourceClassName = createHigherOrderComponent(
( BlockListBlock ) => {
return ( props ) => {
const { name, attributes, wrapperProps = {} } = props;

const { hasInvalidResource } = useHasInvalidSource(
name,
attributes
);

const newWrapperProps = hasInvalidResource
? {
...wrapperProps,
className: clsx(
wrapperProps.className,
'wporg-media-resource-checker-has-invalid-resource'
),
}
: wrapperProps;

return (
<BlockListBlock { ...props } wrapperProps={ newWrapperProps } />
);
};
},
'withInvalidResourceClassName'
);

addFilter(
'editor.BlockEdit',
'wporg-media-resource-checker/with-media-resource-checker',
withMediaResourceChecker
);

addFilter(
'editor.BlockListBlock',
'wporg-media-resource-checker/with-invalid-resource-class-name',
withInvalidResourceClassName
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.wporg-media-resource-checker-has-invalid-resource::before {
content: '';
display: block;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 2;
border: 2px solid #cc1818;
background: rgba(#cc1818, 0.2);
box-sizing: border-box;
}

.wporg-media-resource-checker-toolbar-button svg {
fill: #cc1818;
}

.wporg-media-resource-checker-popover-content {
width: 240px;
word-break: break-word;

p {
margin: 0 0 1em;
}

ul {
margin: 0;
list-style: disc outside;
padding-left: 1.5em;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* WordPress dependencies
*/
import { isBlobURL } from '@wordpress/blob';
import { getAuthority } from '@wordpress/url';

// List of blocks to check.
const BLOCKS_TO_CHECK = [
{
name: 'core/image',
idKey: 'id',
urlKey: 'url',
},
{
name: 'core/video',
idKey: 'id',
urlKey: 'src',
},
{
name: 'core/cover',
idKey: 'id',
urlKey: 'url',
},
];

// List of allowed domain regexes.
export const ALLOWED_DOMAINS = [
{
authority: 'wordpress.org',
regex: /^(.*\.)?wordpress\.org$/,
},
Comment on lines +28 to +31
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We likely should also allow the WordPress.org CDNs here:

Suggested change
{
authority: 'wordpress.org',
regex: /^(.*\.)?wordpress\.org$/,
},
{
authority: 'wordpress.org',
regex: /^(.*\.)?wordpress\.org$/,
},
{
authority: 'wordpress.org',
regex: /^(.*\.)?w\.org$/,
},

Eg; https://s.w.org/images/core/6.9/01-notes.webp?v=24082

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 302493e

Just to be sure, I have distinguished between the authority texts wordpress.org and w.org.

image

{
authority: 'wp.com',
regex: /^(.*\.)?wp\.com$/,
},
];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm wondering if wp.com should be allowed here, since WordPress.org != WordPress.com, and often using wpcom hosted images ends up invalid.

I can imagine this might also be intended for photon images? In which case, allowing ^(.*\.)?wp.com/(.*\.)?wordpress\.org/ could make sense?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I can imagine this might also be intended for photon images?

Yes, that's right. For example, the handbook uses images that are actually delivered from the Photon CDN:

https://github.com/search?q=repo%3AWordPress%2Fdeveloper-plugins-handbook%20wp.com&type=code

In which case, allowing ^(.*\.)?wp.com/(.*\.)?wordpress\.org/ could make sense?

I agree, fixed in e1db353.

Specific examples of allowed URLs can be found here.

https://github.com/WordPress/wordpress.org/pull/536/changes#diff-6b3ed010975581690ee4c331ff822c288910b4e110783cb70ede501d96183e36R66-R70


/**
* Gets the media resource to check for the block.
*
* @param {string} blockName The name of the block.
* @param {Object} attributes The attributes of the block.
* @return {string|null} The media resource to check, or null if the block is not in the list of blocks to check.
*/
export const getBlockMediaResourceToCheck = ( blockName, attributes ) => {
const blockToCheck = BLOCKS_TO_CHECK.find(
( block ) => block.name === blockName
);
if ( ! blockToCheck ) {
return null;
}
return attributes[ blockToCheck.urlKey ];
};

/**
* Checks whether the block has an invalid resource.
*
* @param {string} mediaUrl The media URL to check.
* @param {string} siteUrl The site URL.
* @return {boolean} True if the resource is invalid.
*/
export const isInvalidResource = ( mediaUrl, siteUrl ) => {
if ( ! siteUrl ) {
return false;
}

// If no URL, cannot determine.
if ( ! mediaUrl || isBlobURL( mediaUrl ) ) {
return false;
}

const siteAuthority = getAuthority( siteUrl );
const mediaAuthority = getAuthority( mediaUrl );

// If the authority is the same, it means the resource is from the site.
if ( siteAuthority === mediaAuthority ) {
return false;
}

// Check if the authority is from an allowed domain.
if (
ALLOWED_DOMAINS.some( ( domain ) =>
mediaAuthority.match( domain.regex )
)
) {
return false;
}

// The media is not from an allowed domain.
return true;
};
Loading