Skip to content
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

Verify 'upload_files' capability when displaying upload UI in media blocks #4155

Merged
merged 7 commits into from
Nov 15, 2018
Merged
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 );
imath marked this conversation as resolved.
Show resolved Hide resolved

$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' ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this check is not necessary, to be consistent with other withSelect usage, we should assume the "core" store to be available as it's a dependency of the editor package.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I saw in the comments that it's because of the unit tests. Two suggestions:

  • Avoid running this code in unit tests (test the inner component)
  • Import the @wordpress/core-data in the unit tests to ensure that the store is available.

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' ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as above.

hasUploadPermissions = select( 'core' ).hasUploadPermissions();
}

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