Skip to content

Commit 21e5539

Browse files
authored
VideoPress, Invalid VP URL: Handle case when the URL doesn't contain VP GUID (#42237)
* Create method to get VP url from the file name * Create method to get file name from the url * Handle case when the URL doesn't contain VP GUID * changelog * Add missing gap between buttons * Add test for the getVideoNameFromUrl method * Add more tests * Update changelog * Handle case when the file name doesn't exist * Improve test coverage * Improve test coverage
1 parent 813bb25 commit 21e5539

File tree

6 files changed

+174
-12
lines changed

6 files changed

+174
-12
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
- Handle case when the URL doesn't contain VideoPress GUID
5+
- CSS: Add missing space between CTAs

projects/packages/videopress/src/client/block-editor/blocks/video/components/videopress-uploader/index.js

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ import { __ } from '@wordpress/i18n';
1313
import useResumableUploader from '../../../../../hooks/use-resumable-uploader';
1414
import { uploadFromLibrary } from '../../../../../hooks/use-uploader';
1515
import { isUserConnected } from '../../../../../lib/connection';
16-
import { buildVideoPressURL, pickVideoBlockAttributesFromUrl } from '../../../../../lib/url';
16+
import {
17+
buildVideoPressURL,
18+
buildVideoPressVideoByFileName,
19+
pickVideoBlockAttributesFromUrl,
20+
getVideoNameFromUrl,
21+
} from '../../../../../lib/url';
1722
import { VIDEOPRESS_VIDEO_ALLOWED_MEDIA_TYPES } from '../../constants';
1823
import { PlaceholderWrapper } from '../../edit';
1924
import { VideoPressIcon } from '../icons';
@@ -111,15 +116,25 @@ const VideoPressUploader = ( {
111116
function onSelectURL( videoSource, id ) {
112117
// If the video source is a VideoPress URL, we can use it directly.
113118
const { guid: guidFromSource, url: srcFromSource } = buildVideoPressURL( videoSource );
114-
if ( ! guidFromSource ) {
115-
setUploadErrorDataState( {
116-
data: { message: __( 'Invalid VideoPress URL', 'jetpack-videopress-pkg' ) },
117-
} );
118-
return;
119+
const invalidUrlMessage = __( 'Invalid VideoPress URL', 'jetpack-videopress-pkg' );
120+
121+
if ( guidFromSource ) {
122+
const attrs = pickVideoBlockAttributesFromUrl( srcFromSource );
123+
handleDoneUpload( { ...attrs, guid: guidFromSource, id } );
124+
} else {
125+
// If the video source is not a VideoPress URL, try to build it from the file name.
126+
const videoName = getVideoNameFromUrl( videoSource );
127+
128+
if ( ! videoName ) {
129+
setUploadErrorDataState( { data: { message: invalidUrlMessage } } );
130+
} else {
131+
buildVideoPressVideoByFileName( videoName ).then( attrs => {
132+
attrs
133+
? handleDoneUpload( { ...attrs, id } )
134+
: setUploadErrorDataState( { data: { message: invalidUrlMessage } } );
135+
} );
136+
}
119137
}
120-
121-
const attrs = pickVideoBlockAttributesFromUrl( srcFromSource );
122-
handleDoneUpload( { ...attrs, guid: guidFromSource, id } );
123138
}
124139

125140
const startUpload = file => {

projects/packages/videopress/src/client/block-editor/blocks/video/components/videopress-uploader/style.scss

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,16 @@
4242
transition: width 0.3s ease;
4343
}
4444

45-
&__actions {
45+
&__actions,
46+
&__error-actions {
4647
display: flex;
4748
justify-content: space-between;
4849
align-items: center;
4950
}
51+
52+
&__error-actions {
53+
gap: 16px;
54+
}
5055
}
5156

5257
// Uploader Editor

projects/packages/videopress/src/client/block-editor/blocks/video/components/videopress-uploader/uploader-error.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const UploadError = ( { errorData, onRetry, onCancel } ) => {
3838

3939
return (
4040
<PlaceholderWrapper errorMessage={ message } onNoticeRemove={ onCancel }>
41-
<div className="videopress-uploader__error-actions">
41+
<div className="videopress-uploader-progress__error-actions">
4242
<Button variant="primary" onClick={ onRetry }>
4343
{ __( 'Try again', 'jetpack-videopress-pkg' ) }
4444
</Button>

projects/packages/videopress/src/client/lib/url/index.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import apiFetch from '@wordpress/api-fetch';
12
import { addQueryArgs } from '@wordpress/url';
23
import { VideoBlockAttributes, VideoGUID } from '../../block-editor/blocks/video/types';
34

@@ -159,6 +160,37 @@ export function buildVideoPressURL(
159160
return {};
160161
}
161162

163+
/**
164+
* Search for a VideoPress video by filename in the media library
165+
*
166+
* @param {string} fileName - The name of the video file to search for
167+
* @param {VideoBlockAttributes} attributes - Optional VideoPress URL attributes
168+
* @return {Promise<BuildVideoPressURLProps | null>} The VideoPress URL and GUID if found, null otherwise
169+
*/
170+
export async function buildVideoPressVideoByFileName(
171+
fileName: string,
172+
attributes: VideoBlockAttributes = {}
173+
): Promise< BuildVideoPressURLProps | null > {
174+
try {
175+
const results = await apiFetch< Array< { jetpack_videopress_guid?: string } > >( {
176+
path: `/wp/v2/media?mime_type=video&search=${ encodeURIComponent( fileName ) }`,
177+
} );
178+
179+
const videoFile = results.find( item => item.jetpack_videopress_guid );
180+
181+
if ( videoFile?.jetpack_videopress_guid ) {
182+
return {
183+
url: getVideoPressUrl( videoFile.jetpack_videopress_guid, attributes ),
184+
guid: videoFile.jetpack_videopress_guid,
185+
};
186+
}
187+
188+
return null;
189+
} catch {
190+
return null;
191+
}
192+
}
193+
162194
export const removeFileNameExtension = ( name: string ) => {
163195
return name.replace( /\.[^/.]+$/, '' );
164196
};
@@ -179,6 +211,26 @@ export function getVideoUrlBasedOnPrivacy( guid: VideoGUID, isPrivate: boolean )
179211
return `https://videopress.com/v/${ guid }`;
180212
}
181213

214+
/**
215+
* Extract the video filename with extension from a URL
216+
*
217+
* @param {string} url - The URL containing the video filename
218+
* @return {string} The video filename with extension, or empty string if not found
219+
*/
220+
export function getVideoNameFromUrl( url: string ): string {
221+
try {
222+
const urlObj = new URL( url );
223+
224+
// Split the pathname by '/' and get the last segment
225+
const segments = urlObj.pathname.split( '/' );
226+
const fileName = segments[ segments.length - 1 ];
227+
228+
return fileName || '';
229+
} catch {
230+
return '';
231+
}
232+
}
233+
182234
/**
183235
* Determines if a given URL is a VideoPress URL.
184236
*

projects/packages/videopress/src/client/lib/url/test/index.ts

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
/**
22
* Internal dependencies
33
*/
4-
import { buildVideoPressURL, pickVideoBlockAttributesFromUrl } from '..';
4+
import {
5+
buildVideoPressURL,
6+
pickVideoBlockAttributesFromUrl,
7+
getVideoNameFromUrl,
8+
removeFileNameExtension,
9+
isVideoPressUrl,
10+
} from '..';
511

612
describe( 'buildVideoPressURL', () => {
713
it( 'should return empty object when invalid URL', () => {
@@ -124,3 +130,82 @@ describe( 'pickVideoBlockAttributesFromUrl', () => {
124130
);
125131
} );
126132
} );
133+
134+
describe( 'getVideoNameFromUrl', () => {
135+
it( 'should return empty string when no URL', () => {
136+
expect( getVideoNameFromUrl( '' ) ).toBe( '' );
137+
138+
expect( getVideoNameFromUrl( 'wrong-url' ) ).toBe( '' );
139+
} );
140+
141+
it( 'should return video name from URL', () => {
142+
expect(
143+
getVideoNameFromUrl( 'https://test.wordpres.com/xxxx-photo-2693212/video-file.mp4' )
144+
).toBe( 'video-file.mp4' );
145+
146+
expect( getVideoNameFromUrl( 'https://test.wordpres.com/xxxx-photo-2693212/video-file' ) ).toBe(
147+
'video-file'
148+
);
149+
} );
150+
} );
151+
152+
describe( 'removeFileNameExtension', () => {
153+
it( 'should remove extension from a simple filename', () => {
154+
expect( removeFileNameExtension( 'video.mp4' ) ).toBe( 'video' );
155+
} );
156+
157+
it( 'should remove extension from a filename with multiple dots', () => {
158+
expect( removeFileNameExtension( 'my.awesome.video.mp4' ) ).toBe( 'my.awesome.video' );
159+
} );
160+
161+
it( 'should handle filenames without extension', () => {
162+
expect( removeFileNameExtension( 'video' ) ).toBe( 'video' );
163+
} );
164+
165+
it( 'should handle filenames starting with a dot', () => {
166+
expect( removeFileNameExtension( '.htaccess' ) ).toBe( '' );
167+
} );
168+
169+
it( 'should handle empty string', () => {
170+
expect( removeFileNameExtension( '' ) ).toBe( '' );
171+
} );
172+
} );
173+
174+
describe( 'isVideoPressUrl', () => {
175+
describe( 'should return true for valid VideoPress URLs', () => {
176+
const validUrls = [
177+
'https://videopress.com/v/xyrdcYF4',
178+
'https://videopress.com/v/xyrdcYF4/',
179+
'https://videopress.com/embed/xyrdcYF4',
180+
'https://v.wordpress.com/xyrdcYF4/',
181+
'https://video.wordpress.com/v/xyrdcYF4',
182+
'https://video.wordpress.com/embed/xyrdcYF4/',
183+
'http://videopress.com/v/xyrdcYF4', // HTTP protocol
184+
];
185+
186+
validUrls.forEach( url => {
187+
it( `should validate ${ url }`, () => {
188+
expect( isVideoPressUrl( url ) ).toBe( true );
189+
} );
190+
} );
191+
} );
192+
193+
describe( 'should return false for invalid URLs', () => {
194+
const invalidUrls = [
195+
'https://example.com',
196+
'',
197+
'https://videopress.com/invalid/xyrdcYF4', // Invalid path
198+
'https://videopress.com/v/xyz', // Invalid GUID (too short)
199+
'https://videopress.com/v/xyrdcYF4extra', // Invalid GUID (too long)
200+
'https://videopress.com/v/', // Missing GUID
201+
'https://fakevideo.wordpress.com/v/xyrdcYF4', // Invalid subdomain
202+
'videopress.com/v/xyrdcYF4', // Missing protocol
203+
];
204+
205+
invalidUrls.forEach( url => {
206+
it( `should not validate ${ url || '(empty string)' }`, () => {
207+
expect( isVideoPressUrl( url ) ).toBe( false );
208+
} );
209+
} );
210+
} );
211+
} );

0 commit comments

Comments
 (0)