diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/.gitignore b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/.gitignore new file mode 100644 index 0000000000..504afef81f --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/build/index.asset.php b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/build/index.asset.php new file mode 100644 index 0000000000..1ed00c1871 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/build/index.asset.php @@ -0,0 +1 @@ + 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' => '5969b60b5680025d093d'); diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/build/index.js b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/build/index.js new file mode 100644 index 0000000000..777c9caadc --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/build/index.js @@ -0,0 +1 @@ +(()=>{"use strict";var e,r={566(){function e(r){var o,i,t="";if("string"==typeof r||"number"==typeof r)t+=r;else if("object"==typeof r)if(Array.isArray(r)){var n=r.length;for(o=0;o{const o=(0,w.useSelect)(e=>{const r=e(m.store).getEntityRecord("root","__unstableBase");return r?.home||r?.url||null},[]),i=((e,r)=>{const o=h.find(r=>r.name===e);return o?r[o.urlKey]:null})(e,r),t=i&&((e,r)=>{if(!r)return!1;if(!e||(0,u.isBlobURL)(e))return!1;if(!(0,l.isURL)(e))return!0;const o=(0,l.getAuthority)(r),i=(0,l.getAuthority)(e);if(!i)return!0;if(o===i)return!1;const t=p.find(e=>i.match(e.domainRegex));if(!t)return!0;if(t.pathRegex){const r=new URL(e).pathname;if(!r||!t.pathRegex.test(r))return!0}return!1})(i,o);return{hasInvalidResource:t,siteUrl:o,mediaUrl:i}},f=({BlockEdit:e,siteUrl:r,mediaUrl:i,...t})=>{const a=[(0,l.getAuthority)(r),...p.map(e=>e.authority)];return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(e,{...t}),t.isSelected&&(0,c.jsx)(n.BlockControls,{children:(0,c.jsx)(s.Dropdown,{contentClassName:"",renderToggle:({isOpen:e,onToggle:r})=>(0,c.jsx)(s.ToolbarGroup,{children:(0,c.jsx)(s.ToolbarButton,{"aria-expanded":e,"aria-haspopup":"true",onClick:r,label:(0,o.__)("Media resource error"),icon:d,className:"wporg-media-resource-checker-toolbar-button"})}),renderContent:()=>(0,c.jsxs)("div",{className:"wporg-media-resource-checker-popover-content",children:[(0,c.jsx)("p",{children:(0,o.__)("This media resource is from a domain other than the recommended ones.","wporg-media-resource-checker")}),(0,c.jsx)("p",{children:i&&(0,c.jsx)(s.ExternalLink,{href:i,children:i})}),(0,c.jsx)("p",{children:(0,o.__)("Please use a media resource from the following recommended domains:","wporg-media-resource-checker")}),(0,c.jsx)("ul",{children:a.map(e=>(0,c.jsx)("li",{children:e},e))})]})})})]})},v=(0,t.createHigherOrderComponent)(e=>r=>{const{name:o,attributes:i}=r,{hasInvalidResource:t,siteUrl:n,mediaUrl:s}=g(o,i);return t?(0,c.jsx)(f,{BlockEdit:e,siteUrl:n,mediaUrl:s,...r}):(0,c.jsx)(e,{...r},"edit")},"withMediaResourceChecker"),x=(0,t.createHigherOrderComponent)(e=>o=>{const{name:i,attributes:t,wrapperProps:n={}}=o,{hasInvalidResource:s}=g(i,t),a=s?{...n,className:r(n.className,"wporg-media-resource-checker-has-invalid-resource")}:n;return(0,c.jsx)(e,{...o,wrapperProps:a})},"withInvalidResourceClassName");(0,i.addFilter)("editor.BlockEdit","wporg-media-resource-checker/with-media-resource-checker",v),(0,i.addFilter)("editor.BlockListBlock","wporg-media-resource-checker/with-invalid-resource-class-name",x)}},o={};function i(e){var t=o[e];if(void 0!==t)return t.exports;var n=o[e]={exports:{}};return r[e](n,n.exports,i),n.exports}i.m=r,e=[],i.O=(r,o,t,n)=>{if(!o){var s=1/0;for(l=0;l=n)&&Object.keys(i.O).every(e=>i.O[e](o[c]))?o.splice(c--,1):(a=!1,n0&&e[l-1][2]>n;l--)e[l]=e[l-1];e[l]=[o,t,n]},i.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),(()=>{var e={57:0,350:0};i.O.j=r=>0===e[r];var r=(r,o)=>{var t,n,[s,a,c]=o,d=0;if(s.some(r=>0!==e[r])){for(t in a)i.o(a,t)&&(i.m[t]=a[t]);if(c)var l=c(i)}for(r&&r(o);di(566));t=i.O(t)})(); \ No newline at end of file diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/build/style-index-rtl.css b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/build/style-index-rtl.css new file mode 100644 index 0000000000..438ff039de --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/build/style-index-rtl.css @@ -0,0 +1 @@ +.wporg-media-resource-checker-has-invalid-resource:before{background:rgba(204,24,24,.2);border:2px solid #cc1818;box-sizing:border-box;content:"";display:block;height:100%;right:0;position:absolute;top:0;width:100%;z-index:2}.wporg-media-resource-checker-toolbar-button svg{fill:#cc1818}.wporg-media-resource-checker-popover-content{width:240px;word-break:break-word}.wporg-media-resource-checker-popover-content p{margin:0 0 1em}.wporg-media-resource-checker-popover-content ul{list-style:disc outside;margin:0;padding-right:1.5em} diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/build/style-index.css b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/build/style-index.css new file mode 100644 index 0000000000..eb7c5a61b0 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/build/style-index.css @@ -0,0 +1 @@ +.wporg-media-resource-checker-has-invalid-resource:before{background:rgba(204,24,24,.2);border:2px solid #cc1818;box-sizing:border-box;content:"";display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:2}.wporg-media-resource-checker-toolbar-button svg{fill:#cc1818}.wporg-media-resource-checker-popover-content{width:240px;word-break:break-word}.wporg-media-resource-checker-popover-content p{margin:0 0 1em}.wporg-media-resource-checker-popover-content ul{list-style:disc outside;margin:0;padding-left:1.5em} diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/package.json b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/package.json new file mode 100644 index 0000000000..9263518a9d --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/package.json @@ -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" + } +} diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/src/hooks.js b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/src/hooks.js new file mode 100644 index 0000000000..35ce6785e0 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/src/hooks.js @@ -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, + }; +}; diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/src/index.js b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/src/index.js new file mode 100644 index 0000000000..e655146b25 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/src/index.js @@ -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_RESOURCES } 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_RESOURCES.map( ( resource ) => resource.authority ), + ]; + + return ( + <> + + { props.isSelected && ( + + { + return ( + + + + ); + } } + renderContent={ () => { + return ( +
+

+ { __( + 'This media resource is from a domain other than the recommended ones.', + 'wporg-media-resource-checker' + ) } +

+

+ { mediaUrl && ( + + { mediaUrl } + + ) } +

+

+ { __( + 'Please use a media resource from the following recommended domains:', + 'wporg-media-resource-checker' + ) } +

+
    + { allowedDomainList.map( ( domain ) => ( +
  • { domain }
  • + ) ) } +
+
+ ); + } } + /> +
+ ) } + + ); +}; + +/** + * 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 ? ( + + ) : ( + + ); + }; +}, '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 ( + + ); + }; + }, + '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 +); diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/src/style.scss b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/src/style.scss new file mode 100644 index 0000000000..e5fcaf3a37 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/src/style.scss @@ -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; + } +} diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/src/utils.js b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/src/utils.js new file mode 100644 index 0000000000..fb52e0dc53 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/src/utils.js @@ -0,0 +1,123 @@ +/** + * WordPress dependencies + */ +import { isBlobURL } from '@wordpress/blob'; +import { getAuthority, isURL } 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 resources. +export const ALLOWED_RESOURCES = [ + { + authority: 'wordpress.org', + domainRegex: /^(.*\.)?wordpress\.org$/, + }, + { + authority: 'w.org', + domainRegex: /^(.*\.)?w\.org$/, + }, + { + authority: 'wp.com', + domainRegex: /^(.*\.)?wp\.com$/, + pathRegex: /^\/wordpress\.org\//, + }, +]; + +/** + * 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. + * + * The following URLs are allowed; any other URLs will not be + * recommended as media resource URLs: + * + * - https://wordpress.org/image.jpg + * - https://make.wordpress.org/image.jpg + * - https://w.org/image.jpg + * - https://s.w.org/images/core/6.9/image.jpg + * - https://i0.wp.com/wordpress.org/image.jpg + * + * @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; + } + + // If the media URL doesn't look like a URL, it means the media is not an external resource. + if ( ! isURL( mediaUrl ) ) { + return true; + } + + const siteAuthority = getAuthority( siteUrl ); + const mediaAuthority = getAuthority( mediaUrl ); + + // If the media authority is not set, it means the resource is not a valid URL. + if ( ! mediaAuthority ) { + return true; + } + + // 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. + const allowedResource = ALLOWED_RESOURCES.find( ( resource ) => + mediaAuthority.match( resource.domainRegex ) + ); + + if ( ! allowedResource ) { + return true; + } + + // If pathRegex is defined, also check the path. + if ( allowedResource.pathRegex ) { + const url = new URL( mediaUrl ); + const path = url.pathname; + if ( ! path || ! allowedResource.pathRegex.test( path ) ) { + return true; + } + } + + return false; +}; diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/wporg-media-resource-checker.php b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/wporg-media-resource-checker.php new file mode 100644 index 0000000000..2a87b88065 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/wporg-media-resource-checker/wporg-media-resource-checker.php @@ -0,0 +1,62 @@ +