Skip to content

VideoPress, Invalid VP URL: Handle case when the URL doesn't contain VP GUID #42237

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

Merged
merged 12 commits into from
Mar 10, 2025
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: fixed

- Handle case when the URL doesn't contain VideoPress GUID
- CSS: Add missing space between CTAs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import { __ } from '@wordpress/i18n';
import useResumableUploader from '../../../../../hooks/use-resumable-uploader';
import { uploadFromLibrary } from '../../../../../hooks/use-uploader';
import { isUserConnected } from '../../../../../lib/connection';
import { buildVideoPressURL, pickVideoBlockAttributesFromUrl } from '../../../../../lib/url';
import {
buildVideoPressURL,
buildVideoPressVideoByFileName,
pickVideoBlockAttributesFromUrl,
getVideoNameFromUrl,
} from '../../../../../lib/url';
import { VIDEOPRESS_VIDEO_ALLOWED_MEDIA_TYPES } from '../../constants';
import { PlaceholderWrapper } from '../../edit';
import { VideoPressIcon } from '../icons';
Expand Down Expand Up @@ -111,15 +116,25 @@ const VideoPressUploader = ( {
function onSelectURL( videoSource, id ) {
// If the video source is a VideoPress URL, we can use it directly.
const { guid: guidFromSource, url: srcFromSource } = buildVideoPressURL( videoSource );
if ( ! guidFromSource ) {
setUploadErrorDataState( {
data: { message: __( 'Invalid VideoPress URL', 'jetpack-videopress-pkg' ) },
} );
return;
const invalidUrlMessage = __( 'Invalid VideoPress URL', 'jetpack-videopress-pkg' );

if ( guidFromSource ) {
const attrs = pickVideoBlockAttributesFromUrl( srcFromSource );
handleDoneUpload( { ...attrs, guid: guidFromSource, id } );
} else {
// If the video source is not a VideoPress URL, try to build it from the file name.
const videoName = getVideoNameFromUrl( videoSource );

if ( ! videoName ) {
setUploadErrorDataState( { data: { message: invalidUrlMessage } } );
} else {
buildVideoPressVideoByFileName( videoName ).then( attrs => {
attrs
? handleDoneUpload( { ...attrs, id } )
: setUploadErrorDataState( { data: { message: invalidUrlMessage } } );
} );
}
}

const attrs = pickVideoBlockAttributesFromUrl( srcFromSource );
handleDoneUpload( { ...attrs, guid: guidFromSource, id } );
}

const startUpload = file => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,16 @@
transition: width 0.3s ease;
}

&__actions {
&__actions,
&__error-actions {
display: flex;
justify-content: space-between;
align-items: center;
}

&__error-actions {
gap: 16px;
}
}

// Uploader Editor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const UploadError = ( { errorData, onRetry, onCancel } ) => {

return (
<PlaceholderWrapper errorMessage={ message } onNoticeRemove={ onCancel }>
<div className="videopress-uploader__error-actions">
<div className="videopress-uploader-progress__error-actions">
<Button variant="primary" onClick={ onRetry }>
{ __( 'Try again', 'jetpack-videopress-pkg' ) }
</Button>
Expand Down
52 changes: 52 additions & 0 deletions projects/packages/videopress/src/client/lib/url/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
import { VideoBlockAttributes, VideoGUID } from '../../block-editor/blocks/video/types';

Expand Down Expand Up @@ -159,6 +160,37 @@ export function buildVideoPressURL(
return {};
}

/**
* Search for a VideoPress video by filename in the media library
*
* @param {string} fileName - The name of the video file to search for
* @param {VideoBlockAttributes} attributes - Optional VideoPress URL attributes
* @return {Promise<BuildVideoPressURLProps | null>} The VideoPress URL and GUID if found, null otherwise
*/
export async function buildVideoPressVideoByFileName(
fileName: string,
attributes: VideoBlockAttributes = {}
): Promise< BuildVideoPressURLProps | null > {
try {
const results = await apiFetch< Array< { jetpack_videopress_guid?: string } > >( {
path: `/wp/v2/media?mime_type=video&search=${ encodeURIComponent( fileName ) }`,
} );

const videoFile = results.find( item => item.jetpack_videopress_guid );

if ( videoFile?.jetpack_videopress_guid ) {
return {
url: getVideoPressUrl( videoFile.jetpack_videopress_guid, attributes ),
guid: videoFile.jetpack_videopress_guid,
};
}

return null;
} catch {
return null;
}
}

export const removeFileNameExtension = ( name: string ) => {
return name.replace( /\.[^/.]+$/, '' );
};
Expand All @@ -179,6 +211,26 @@ export function getVideoUrlBasedOnPrivacy( guid: VideoGUID, isPrivate: boolean )
return `https://videopress.com/v/${ guid }`;
}

/**
* Extract the video filename with extension from a URL
*
* @param {string} url - The URL containing the video filename
* @return {string} The video filename with extension, or empty string if not found
*/
export function getVideoNameFromUrl( url: string ): string {
try {
const urlObj = new URL( url );

// Split the pathname by '/' and get the last segment
const segments = urlObj.pathname.split( '/' );
const fileName = segments[ segments.length - 1 ];

return fileName || '';
} catch {
return '';
}
}

/**
* Determines if a given URL is a VideoPress URL.
*
Expand Down
87 changes: 86 additions & 1 deletion projects/packages/videopress/src/client/lib/url/test/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
/**
* Internal dependencies
*/
import { buildVideoPressURL, pickVideoBlockAttributesFromUrl } from '..';
import {
buildVideoPressURL,
pickVideoBlockAttributesFromUrl,
getVideoNameFromUrl,
removeFileNameExtension,
isVideoPressUrl,
} from '..';

describe( 'buildVideoPressURL', () => {
it( 'should return empty object when invalid URL', () => {
Expand Down Expand Up @@ -124,3 +130,82 @@ describe( 'pickVideoBlockAttributesFromUrl', () => {
);
} );
} );

describe( 'getVideoNameFromUrl', () => {
it( 'should return empty string when no URL', () => {
expect( getVideoNameFromUrl( '' ) ).toBe( '' );

expect( getVideoNameFromUrl( 'wrong-url' ) ).toBe( '' );
} );

it( 'should return video name from URL', () => {
expect(
getVideoNameFromUrl( 'https://test.wordpres.com/xxxx-photo-2693212/video-file.mp4' )
).toBe( 'video-file.mp4' );

expect( getVideoNameFromUrl( 'https://test.wordpres.com/xxxx-photo-2693212/video-file' ) ).toBe(
'video-file'
);
} );
} );

describe( 'removeFileNameExtension', () => {
it( 'should remove extension from a simple filename', () => {
expect( removeFileNameExtension( 'video.mp4' ) ).toBe( 'video' );
} );

it( 'should remove extension from a filename with multiple dots', () => {
expect( removeFileNameExtension( 'my.awesome.video.mp4' ) ).toBe( 'my.awesome.video' );
} );

it( 'should handle filenames without extension', () => {
expect( removeFileNameExtension( 'video' ) ).toBe( 'video' );
} );

it( 'should handle filenames starting with a dot', () => {
expect( removeFileNameExtension( '.htaccess' ) ).toBe( '' );
} );

it( 'should handle empty string', () => {
expect( removeFileNameExtension( '' ) ).toBe( '' );
} );
} );

describe( 'isVideoPressUrl', () => {
describe( 'should return true for valid VideoPress URLs', () => {
const validUrls = [
'https://videopress.com/v/xyrdcYF4',
'https://videopress.com/v/xyrdcYF4/',
'https://videopress.com/embed/xyrdcYF4',
'https://v.wordpress.com/xyrdcYF4/',
'https://video.wordpress.com/v/xyrdcYF4',
'https://video.wordpress.com/embed/xyrdcYF4/',
'http://videopress.com/v/xyrdcYF4', // HTTP protocol
];

validUrls.forEach( url => {
it( `should validate ${ url }`, () => {
expect( isVideoPressUrl( url ) ).toBe( true );
} );
} );
} );

describe( 'should return false for invalid URLs', () => {
const invalidUrls = [
'https://example.com',
'',
'https://videopress.com/invalid/xyrdcYF4', // Invalid path
'https://videopress.com/v/xyz', // Invalid GUID (too short)
'https://videopress.com/v/xyrdcYF4extra', // Invalid GUID (too long)
'https://videopress.com/v/', // Missing GUID
'https://fakevideo.wordpress.com/v/xyrdcYF4', // Invalid subdomain
'videopress.com/v/xyrdcYF4', // Missing protocol
];

invalidUrls.forEach( url => {
it( `should not validate ${ url || '(empty string)' }`, () => {
expect( isVideoPressUrl( url ) ).toBe( false );
} );
} );
} );
} );
Loading