-
Notifications
You must be signed in to change notification settings - Fork 189
Add Media Resource Checker plugin #536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from 1 commit
d12ab9f
70d3d65
87d6397
9d72889
302493e
e1db353
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We likely should also allow the WordPress.org CDNs here:
Suggested change
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| authority: 'wp.com', | ||||||||||||||||||||||||||
| regex: /^(.*\.)?wp\.com$/, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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
I agree, fixed in e1db353. Specific examples of allowed URLs can be found here. |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * 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; | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||

There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
/.gitignoreor just leave it in the project-specific area since it doesn't matter all that much.There was a problem hiding this comment.
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.