diff --git a/docs/data/data-core.md b/docs/data/data-core.md index 5310fe2489c81..2a98c3bb5d397 100644 --- a/docs/data/data-core.md +++ b/docs/data/data-core.md @@ -136,6 +136,18 @@ get back from the oEmbed preview API. Is the preview for the URL an oEmbed link fallback. +### hasUploadPermissions + +Return Upload Permissions. + +*Parameters* + + * state: State tree. + +*Returns* + +Upload Permissions. + ## Actions ### receiveUserQuery @@ -193,4 +205,12 @@ Action triggered to save an entity record. * kind: Kind of the received entity. * name: Name of the received entity. - * record: Record to be saved. \ No newline at end of file + * record: Record to be saved. + +### receiveUploadPermissions + +Returns an action object used in signalling that Upload permissions have been received. + +*Parameters* + + * hasUploadPermissions: Does the user have permission to upload files? \ No newline at end of file diff --git a/lib/client-assets.php b/lib/client-assets.php index 71fb20403897e..18586b1aa4a81 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -908,12 +908,22 @@ function gutenberg_preload_api_request( $memo, $path ) { return $memo; } + $method = 'GET'; + if ( is_array( $path ) && 2 === count( $path ) ) { + $method = end( $path ); + $path = reset( $path ); + + if ( ! in_array( $method, array( 'GET', 'OPTIONS' ), true ) ) { + $method = 'GET'; + } + } + $path_parts = parse_url( $path ); if ( false === $path_parts ) { return $memo; } - $request = new WP_REST_Request( 'GET', $path_parts['path'] ); + $request = new WP_REST_Request( $method, $path_parts['path'] ); if ( ! empty( $path_parts['query'] ) ) { parse_str( $path_parts['query'], $query_params ); $request->set_query_params( $query_params ); @@ -928,10 +938,19 @@ function gutenberg_preload_api_request( $memo, $path ) { $data['_links'] = $links; } - $memo[ $path ] = array( - 'body' => $data, - 'headers' => $response->headers, - ); + if ( 'OPTIONS' === $method ) { + $response = rest_send_allow_header( $response, $server, $request ); + + $memo[ $method ][ $path ] = array( + 'body' => $data, + 'headers' => $response->headers, + ); + } else { + $memo[ $path ] = array( + 'body' => $data, + 'headers' => $response->headers, + ); + } } return $memo; @@ -1431,6 +1450,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { sprintf( '/wp/v2/%s/%s?context=edit', $rest_base, $post->ID ), sprintf( '/wp/v2/types/%s?context=edit', $post_type ), sprintf( '/wp/v2/users/me?post_type=%s&context=edit', $post_type ), + array( '/wp/v2/media', 'OPTIONS' ), ); /** diff --git a/packages/api-fetch/src/middlewares/preloading.js b/packages/api-fetch/src/middlewares/preloading.js index 78f431a984d98..020cb913fced1 100644 --- a/packages/api-fetch/src/middlewares/preloading.js +++ b/packages/api-fetch/src/middlewares/preloading.js @@ -28,12 +28,14 @@ const createPreloadingMiddleware = ( preloadedData ) => ( options, next ) => { } const { parse = true } = options; - if ( typeof options.path === 'string' && parse ) { + if ( typeof options.path === 'string' ) { const method = options.method || 'GET'; const path = getStablePath( options.path ); - if ( 'GET' === method && preloadedData[ path ] ) { + if ( parse && 'GET' === method && preloadedData[ path ] ) { return Promise.resolve( preloadedData[ path ].body ); + } else if ( 'OPTIONS' === method && preloadedData[ method ][ path ] ) { + return Promise.resolve( preloadedData[ method ][ path ] ); } } diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 079ee7ad7eadc..ebbcfb7e76150 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -127,3 +127,17 @@ export function* saveEntityRecord( kind, name, record ) { return updatedRecord; } + +/** + * Returns an action object used in signalling that Upload permissions have been received. + * + * @param {boolean} hasUploadPermissions Does the user have permission to upload files? + * + * @return {Object} Action object. + */ +export function receiveUploadPermissions( hasUploadPermissions ) { + return { + type: 'RECEIVE_UPLOAD_PERMISSIONS', + hasUploadPermissions, + }; +} diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 6d286514a10f6..d37969cbdae14 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -217,6 +217,23 @@ export function embedPreviews( state = {}, action ) { return state; } +/** + * Reducer managing Upload permissions. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function hasUploadPermissions( state = {}, action ) { + switch ( action.type ) { + case 'RECEIVE_UPLOAD_PERMISSIONS': + return action.hasUploadPermissions; + } + + return state; +} + export default combineReducers( { terms, users, @@ -224,4 +241,5 @@ export default combineReducers( { themeSupports, entities, embedPreviews, + hasUploadPermissions, } ); diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 6f6cd0e1d0a8c..b8bd4ed238285 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find } from 'lodash'; +import { find, includes, get, hasIn } from 'lodash'; /** * WordPress dependencies @@ -16,6 +16,7 @@ import { receiveEntityRecords, receiveThemeSupports, receiveEmbedPreview, + receiveUploadPermissions, } from './actions'; import { getKindEntities } from './entities'; import { apiFetch } from './controls'; @@ -97,3 +98,23 @@ export function* getEmbedPreview( url ) { yield receiveEmbedPreview( url, false ); } } + +/** + * Requests Upload Permissions from the REST API. + */ +export function* hasUploadPermissions() { + const response = yield apiFetch( { path: '/wp/v2/media', method: 'OPTIONS', parse: false } ); + + let allowHeader; + if ( hasIn( response, [ 'headers', 'get' ] ) ) { + // If the request is fetched using the fetch api, the header can be + // retrieved using the 'get' method. + allowHeader = response.headers.get( 'allow' ); + } else { + // If the request was preloaded server-side and is returned by the + // preloading middleware, the header will be a simple property. + allowHeader = get( response, [ 'headers', 'Allow' ], '' ); + } + + yield receiveUploadPermissions( includes( allowHeader, 'POST' ) ); +} diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index 57e8a758ca392..95e9f867aa3c0 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -169,3 +169,14 @@ export function isPreviewEmbedFallback( state, url ) { } return preview.html === oEmbedLinkCheck; } + +/** + * Return Upload Permissions. + * + * @param {Object} state State tree. + * + * @return {boolean} Upload Permissions. + */ +export function hasUploadPermissions( state ) { + return state.hasUploadPermissions; +} diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 55b798561a602..e5f5aaf4478d0 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -27,6 +27,7 @@ export { export { default as ServerSideRender } from './server-side-render'; export { default as MediaPlaceholder } from './media-placeholder'; export { default as MediaUpload } from './media-upload'; +export { default as MediaUploadCheck } from './media-upload/check'; export { default as URLInput } from './url-input'; export { default as URLInputButton } from './url-input/button'; export { default as URLPopover } from './url-popover'; diff --git a/packages/editor/src/components/media-placeholder/index.js b/packages/editor/src/components/media-placeholder/index.js index 5ac485063b477..4334aa3b5a2d2 100644 --- a/packages/editor/src/components/media-placeholder/index.js +++ b/packages/editor/src/components/media-placeholder/index.js @@ -17,11 +17,14 @@ import { } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; +import { compose } from '@wordpress/compose'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies */ import MediaUpload from '../media-upload'; +import MediaUploadCheck from '../media-upload/check'; import URLPopover from '../url-popover'; import { mediaUpload } from '../../utils/'; @@ -132,6 +135,7 @@ class MediaPlaceholder extends Component { multiple = false, notices, allowedTypes = [], + hasUploadPermissions, } = this.props; const { @@ -141,6 +145,11 @@ class MediaPlaceholder extends Component { let instructions = labels.instructions || ''; let title = labels.title || ''; + + if ( ! hasUploadPermissions && ! onSelectURL ) { + instructions = __( 'To edit this block, you need permission to upload media.' ); + } + if ( ! instructions || ! title ) { const isOneType = 1 === allowedTypes.length; const isAudio = isOneType && 'audio' === allowedTypes[ 0 ]; @@ -148,14 +157,26 @@ class MediaPlaceholder extends Component { const isVideo = isOneType && 'video' === allowedTypes[ 0 ]; if ( ! instructions ) { - instructions = __( 'Drag a media file, upload a new one or select a file from your library.' ); + if ( hasUploadPermissions ) { + instructions = __( 'Drag a media file, upload a new one or select a file from your library.' ); - if ( isAudio ) { - instructions = __( 'Drag an audio, upload a new one or select a file from your library.' ); - } else if ( isImage ) { - instructions = __( 'Drag an image, upload a new one or select a file from your library.' ); - } else if ( isVideo ) { - instructions = __( 'Drag a video, upload a new one or select a file from your library.' ); + if ( isAudio ) { + instructions = __( 'Drag an audio, upload a new one or select a file from your library.' ); + } else if ( isImage ) { + instructions = __( 'Drag an image, upload a new one or select a file from your library.' ); + } else if ( isVideo ) { + instructions = __( 'Drag a video, upload a new one or select a file from your library.' ); + } + } else if ( ! hasUploadPermissions && onSelectURL ) { + instructions = __( 'Given your current role, you can only link a media file, you cannot upload.' ); + + if ( isAudio ) { + instructions = __( 'Given your current role, you can only link an audio, you cannot upload.' ); + } else if ( isImage ) { + instructions = __( 'Given your current role, you can only link an image, you cannot upload.' ); + } else if ( isVideo ) { + instructions = __( 'Given your current role, you can only link a video, you cannot upload.' ); + } } } @@ -180,35 +201,37 @@ class MediaPlaceholder extends Component { className={ classnames( 'editor-media-placeholder', className ) } notices={ notices } > - - - { __( 'Upload' ) } - - ( - - ) } - /> + + + + { __( 'Upload' ) } + + ( + + ) } + /> + { onSelectURL && (
- ) } - value={ featuredImageId } - /> + + ( + + ) } + value={ featuredImageId } + /> + } { !! featuredImageId && media && ! media.isLoading && - ( - - ) } - /> - } - { ! featuredImageId && -
+ ( - ) } /> + + } + { ! featuredImageId && +
+ + ( + + ) } + /> +
} { !! featuredImageId && - + + + }
diff --git a/packages/format-library/src/image/index.js b/packages/format-library/src/image/index.js index 7deae883d3b3b..c26bcca626a5d 100644 --- a/packages/format-library/src/image/index.js +++ b/packages/format-library/src/image/index.js @@ -3,9 +3,9 @@ */ import { Path, SVG } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { Fragment, Component } from '@wordpress/element'; +import { Component } from '@wordpress/element'; import { insertObject } from '@wordpress/rich-text'; -import { MediaUpload, RichTextInserterItem } from '@wordpress/editor'; +import { MediaUpload, RichTextInserterItem, MediaUploadCheck } from '@wordpress/editor'; const ALLOWED_MEDIA_TYPES = [ 'image' ]; @@ -46,7 +46,7 @@ export const image = { const { value, onChange } = this.props; return ( - + } @@ -73,7 +73,7 @@ export const image = { return null; } } /> } - + ); } },