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

Add validation to link format in RichText component #11286

Merged
merged 8 commits into from
Nov 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 33 additions & 18 deletions packages/format-library/src/link/inline.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
Expand All @@ -23,6 +28,7 @@ import { URLInput, URLPopover } from '@wordpress/editor';
* Internal dependencies
*/
import PositionedAtSelection from './positioned-at-selection';
import { isValidHref } from './utils';

const stopKeyPropagation = ( event ) => event.stopPropagation();

Expand Down Expand Up @@ -78,23 +84,30 @@ const LinkEditor = ( { value, onChangeInputValue, onKeyDown, submitLink, autocom
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions */
);

const LinkViewer = ( { url, editLink } ) => (
// Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar
/* eslint-disable jsx-a11y/no-static-element-interactions */
<div
className="editor-format-toolbar__link-container-content"
onKeyPress={ stopKeyPropagation }
>
<ExternalLink
className="editor-format-toolbar__link-container-value"
href={ url }
const LinkViewer = ( { url, editLink } ) => {
const prependedURL = prependHTTP( url );
const linkClassName = classnames( 'editor-format-toolbar__link-container-value', {
'has-invalid-link': ! isValidHref( prependedURL ),
} );

return (
// Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar
/* eslint-disable jsx-a11y/no-static-element-interactions */
<div
className="editor-format-toolbar__link-container-content"
onKeyPress={ stopKeyPropagation }
>
{ filterURLForDisplay( safeDecodeURI( url ) ) }
</ExternalLink>
<IconButton icon="edit" label={ __( 'Edit' ) } onClick={ editLink } />
</div>
/* eslint-enable jsx-a11y/no-static-element-interactions */
);
<ExternalLink
className={ linkClassName }
href={ url }
>
{ filterURLForDisplay( safeDecodeURI( url ) ) }
</ExternalLink>
<IconButton icon="edit" label={ __( 'Edit' ) } onClick={ editLink } />
</div>
/* eslint-enable jsx-a11y/no-static-element-interactions */
);
};

class InlineLinkUI extends Component {
constructor() {
Expand Down Expand Up @@ -178,8 +191,10 @@ class InlineLinkUI extends Component {

this.resetState();

if ( isActive ) {
speak( __( 'Link edited' ), 'assertive' );
if ( ! isValidHref( url ) ) {
speak( __( 'Warning: the link has been inserted but may have errors. Please test it.' ), 'assertive' );
} else if ( isActive ) {
speak( __( 'Link edited.' ), 'assertive' );
} else {
speak( __( 'Link inserted' ), 'assertive' );
}
Expand Down
4 changes: 4 additions & 0 deletions packages/format-library/src/link/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@
white-space: nowrap;
min-width: 150px;
max-width: 500px;

&.has-invalid-link {
color: $alert-red;
gziolo marked this conversation as resolved.
Show resolved Hide resolved
}
}
76 changes: 76 additions & 0 deletions packages/format-library/src/link/test/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

/**
* Internal dependencies
*/
import {
isValidHref,
} from '../utils';

describe( 'isValidHref', () => {
it( 'returns true if the href cannot be recognised as a url or an anchor link', () => {
expect( isValidHref( 'notaurloranchorlink' ) ).toBe( true );
} );

it( 'returns false if the href is not specified', () => {
expect( isValidHref() ).toBe( false );
expect( isValidHref( '' ) ).toBe( false );
expect( isValidHref( ' ' ) ).toBe( false );
} );

describe( 'URLs beginning with a protocol', () => {
it( 'returns true for valid URLs', () => {
expect( isValidHref( 'tel:+123456789' ) ).toBe( true );
expect( isValidHref( 'mailto:test@somewhere.com' ) ).toBe( true );
expect( isValidHref( 'file:///c:/WINDOWS/winamp.exe' ) ).toBe( true );
expect( isValidHref( 'http://test.com' ) ).toBe( true );
expect( isValidHref( 'https://test.com' ) ).toBe( true );
expect( isValidHref( 'http://test-with-hyphen.com' ) ).toBe( true );
expect( isValidHref( 'http://test.com/' ) ).toBe( true );
expect( isValidHref( 'http://test.com/with/path/separators' ) ).toBe( true );
expect( isValidHref( 'http://test.com/with?query=string&params' ) ).toBe( true );
} );

it( 'returns false for invalid urls', () => {
expect( isValidHref( 'tel:+12 345 6789' ) ).toBe( false );
expect( isValidHref( 'mailto:test @ somewhere.com' ) ).toBe( false );
expect( isValidHref( 'mailto: test@somewhere.com' ) ).toBe( false );
expect( isValidHref( 'ht#tp://this/is/invalid' ) ).toBe( false );
expect( isValidHref( 'ht#tp://th&is/is/invalid' ) ).toBe( false );
expect( isValidHref( 'http://?test.com' ) ).toBe( false );
expect( isValidHref( 'http://#test.com' ) ).toBe( false );
expect( isValidHref( 'http://test.com?double?params' ) ).toBe( false );
expect( isValidHref( 'http://test.com#double#anchor' ) ).toBe( false );
expect( isValidHref( 'http://test.com?path/after/params' ) ).toBe( false );
expect( isValidHref( 'http://test.com#path/after/fragment' ) ).toBe( false );
} );

it( 'returns false if the URL has whitespace', () => {
expect( isValidHref( 'http:/ /test.com' ) ).toBe( false );
expect( isValidHref( 'http://te st.com' ) ).toBe( false );
expect( isValidHref( 'http:// test.com' ) ).toBe( false );
expect( isValidHref( 'http://test.c om' ) ).toBe( false );
expect( isValidHref( 'http://test.com/ee ee/' ) ).toBe( false );
expect( isValidHref( 'http://test.com/eeee?qwd qwdw' ) ).toBe( false );
expect( isValidHref( 'http://test.com/eeee#qwd qwdw' ) ).toBe( false );
} );
} );

describe( 'Anchor links', () => {
it( 'returns true for valid anchor links', () => {
expect( isValidHref( '#yesitis' ) ).toBe( true );
expect( isValidHref( '#yes_it_is' ) ).toBe( true );
expect( isValidHref( '#yes~it~is' ) ).toBe( true );
expect( isValidHref( '#yes-it-is' ) ).toBe( true );
} );

it( 'returns false for invalid anchor links', () => {
expect( isValidHref( '' ) ).toBe( false );
expect( isValidHref( '#no-it-isnt#' ) ).toBe( false );
expect( isValidHref( '#no-it-#isnt' ) ).toBe( false );
expect( isValidHref( '#no-it-isnt?' ) ).toBe( false );
expect( isValidHref( '#no-it isnt' ) ).toBe( false );
expect( isValidHref( '#no-it-isnt/' ) ).toBe( false );
} );
} );
} );

74 changes: 74 additions & 0 deletions packages/format-library/src/link/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* External dependencies
*/
import { startsWith } from 'lodash';

/**
* WordPress dependencies
*/
import {
getProtocol,
isValidProtocol,
getAuthority,
isValidAuthority,
getPath,
isValidPath,
getQueryString,
isValidQueryString,
getFragment,
isValidFragment,
} from '@wordpress/url';

/**
* Check for issues with the provided href.
*
* @param {string} href The href.
*
* @return {boolean} Is the href invalid?
*/
export function isValidHref( href ) {
if ( ! href ) {
return false;
}

const trimmedHref = href.trim();

if ( ! trimmedHref ) {
return false;
}

// Does the href start with something that looks like a url protocol?
if ( /^\S+:/.test( trimmedHref ) ) {
const protocol = getProtocol( trimmedHref );
if ( ! isValidProtocol( protocol ) ) {
return false;
}

const authority = getAuthority( trimmedHref );
if ( ! isValidAuthority( authority ) ) {
return false;
}

const path = getPath( trimmedHref );
if ( path && ! isValidPath( path ) ) {
return false;
}

const queryString = getQueryString( trimmedHref );
if ( queryString && ! isValidQueryString( queryString ) ) {
return false;
}

const fragment = getFragment( trimmedHref );
if ( fragment && ! isValidFragment( trimmedHref ) ) {
return false;
}
}

// Validate anchor links.
if ( startsWith( trimmedHref, '#' ) && ! isValidFragment( trimmedHref ) ) {
return false;
}

return true;
}
21 changes: 18 additions & 3 deletions packages/url/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
## 2.3.0 (Unreleased)

### New Features

- Added `getProtocol`.
- Added `isValidProtocol`.
- Added `getAuthority`
- Added `isValidAuthority`.
- Added `getPath`.
- Added `isValidPath`.
- Added `getQueryString`.
- Added `isValidQueryString`.
- Added `getFragment`.
- Added `isValidFragment`.

## 2.2.0 (2018-10-29)

### Features
### New Features

- Added `getQueryArg`.
- Added `hasQueryArg`.
- Added `removeQueryArgs`.

## 2.1.0 (2018-10-16)

### Features
### New Feature

- Added `safeDecodeURI`.

## 2.0.1 (2018-09-30)

### Bug Fixes
### Bug Fix

- Fix typo in the `qs` dependency definition in the `package.json`

Expand Down
Loading