Skip to content

Commit

Permalink
Verify 'upload_files' capability when displaying upload UI in media b…
Browse files Browse the repository at this point in the history
…locks (#4155)

* Use OPTIONS Rest API Media Request to get user `upload_file` capability

Edit the preloaded path and adapt the way apiFetch is preloading the requests using the OPTIONS method.
Add a hasUploadPermissions property to core/data

* Introduce the MediaUploadCheck component

Start using it into the MediaPlaceHolder to inform the contributor role he cannot upload media. If the block supports selecting an URL, the instruction will inform the contributor role he can only add a link to a media.

* Display a message to the contributor about the featured image

As it is not possible to fetch the Featured image for this role, no matter the context of the REST request, display a message to inform him managing the featured image needs the upload files cap.

* Enforce strict format of the preloaded path when an array is used

This is to bring into Gutenberg the improvement @danielbachhuber committed in WordPress 5.0 branch
See https://core.trac.wordpress.org/changeset/43833

* Make sure the Inline Image is only available to users with the upload_files cap

* Update generated data-core docs

* Apply @gziolo recommandations and fix a failing unit test
  • Loading branch information
imath authored and gziolo committed Nov 15, 2018
1 parent c6bbfb5 commit bcf55ff
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 89 deletions.
22 changes: 21 additions & 1 deletion docs/data/data-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
* 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?
30 changes: 25 additions & 5 deletions lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand All @@ -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;
Expand Down Expand Up @@ -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' ),
);

/**
Expand Down
6 changes: 4 additions & 2 deletions packages/api-fetch/src/middlewares/preloading.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ] );
}
}

Expand Down
14 changes: 14 additions & 0 deletions packages/core-data/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
18 changes: 18 additions & 0 deletions packages/core-data/src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,29 @@ 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,
taxonomies,
themeSupports,
entities,
embedPreviews,
hasUploadPermissions,
} );
23 changes: 22 additions & 1 deletion packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { find } from 'lodash';
import { find, includes, get, hasIn } from 'lodash';

/**
* WordPress dependencies
Expand All @@ -16,6 +16,7 @@ import {
receiveEntityRecords,
receiveThemeSupports,
receiveEmbedPreview,
receiveUploadPermissions,
} from './actions';
import { getKindEntities } from './entities';
import { apiFetch } from './controls';
Expand Down Expand Up @@ -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' ) );
}
11 changes: 11 additions & 0 deletions packages/core-data/src/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions packages/editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
111 changes: 74 additions & 37 deletions packages/editor/src/components/media-placeholder/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/';

Expand Down Expand Up @@ -132,6 +135,7 @@ class MediaPlaceholder extends Component {
multiple = false,
notices,
allowedTypes = [],
hasUploadPermissions,
} = this.props;

const {
Expand All @@ -141,21 +145,38 @@ 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 ];
const isImage = isOneType && 'image' === allowedTypes[ 0 ];
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.' );
}
}
}

Expand All @@ -180,35 +201,37 @@ class MediaPlaceholder extends Component {
className={ classnames( 'editor-media-placeholder', className ) }
notices={ notices }
>
<DropZone
onFilesDrop={ this.onFilesUpload }
onHTMLDrop={ onHTMLDrop }
/>
<FormFileUpload
isLarge
className="editor-media-placeholder__button"
onChange={ this.onUpload }
accept={ accept }
multiple={ multiple }
>
{ __( 'Upload' ) }
</FormFileUpload>
<MediaUpload
gallery={ multiple && this.onlyAllowsImages() }
multiple={ multiple }
onSelect={ onSelect }
allowedTypes={ allowedTypes }
value={ value.id }
render={ ( { open } ) => (
<Button
isLarge
className="editor-media-placeholder__button"
onClick={ open }
>
{ __( 'Media Library' ) }
</Button>
) }
/>
<MediaUploadCheck>
<DropZone
onFilesDrop={ this.onFilesUpload }
onHTMLDrop={ onHTMLDrop }
/>
<FormFileUpload
isLarge
className="editor-media-placeholder__button"
onChange={ this.onUpload }
accept={ accept }
multiple={ multiple }
>
{ __( 'Upload' ) }
</FormFileUpload>
<MediaUpload
gallery={ multiple && this.onlyAllowsImages() }
multiple={ multiple }
onSelect={ onSelect }
allowedTypes={ allowedTypes }
value={ value.id }
render={ ( { open } ) => (
<Button
isLarge
className="editor-media-placeholder__button"
onClick={ open }
>
{ __( 'Media Library' ) }
</Button>
) }
/>
</MediaUploadCheck>
{ onSelectURL && (
<div className="editor-media-placeholder__url-input-container">
<Button
Expand All @@ -234,4 +257,18 @@ class MediaPlaceholder extends Component {
}
}

export default withFilters( 'editor.MediaPlaceholder' )( MediaPlaceholder );
const applyWithSelect = withSelect( ( select ) => {
let hasUploadPermissions = false;
if ( undefined !== select( 'core' ) ) {
hasUploadPermissions = select( 'core' ).hasUploadPermissions();
}

return {
hasUploadPermissions: hasUploadPermissions,
};
} );

export default compose(
applyWithSelect,
withFilters( 'editor.MediaPlaceholder' ),
)( MediaPlaceholder );
19 changes: 19 additions & 0 deletions packages/editor/src/components/media-upload/check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* WordPress dependencies
*/
import { withSelect } from '@wordpress/data';

export function MediaUploadCheck( { hasUploadPermissions, fallback = null, children } ) {
return hasUploadPermissions ? children : fallback;
}

export default withSelect( ( select ) => {
let hasUploadPermissions = false;
if ( undefined !== select( 'core' ) ) {
hasUploadPermissions = select( 'core' ).hasUploadPermissions();
}

return {
hasUploadPermissions: hasUploadPermissions,
};
} )( MediaUploadCheck );
Loading

0 comments on commit bcf55ff

Please sign in to comment.