feat(client): validate MS transcript links (#51445)

Co-authored-by: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com>
This commit is contained in:
Oliver Eyton-Williams
2023-08-31 14:29:59 +02:00
committed by GitHub
parent ddd60e7e2d
commit f6b970fbee
4 changed files with 112 additions and 29 deletions
@@ -450,7 +450,8 @@
"link-li-3": "If you do not have a transcript link, click the \"Create link\" button to create one.",
"link-li-4": "Click the \"Copy link\" button to copy the transcript URL.",
"link-li-5": "Paste the URL into the input below and click \"Link Account\".",
"transcript-label": "Your Microsoft Transcript Link"
"transcript-label": "Your Microsoft Transcript Link",
"invalid-transcript": "Your transcript link is not correct, it should have the following form: <1>https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID</1> - check the UPPERCASE items in your link are correct."
}
},
"donate": {
@@ -3,16 +3,17 @@ import {
Button,
FormGroup,
ControlLabel,
FormControl
FormControl,
HelpBlock
} from '@freecodecamp/react-bootstrap';
import { connect } from 'react-redux';
import { ConnectedProps, connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
import type { TFunction } from 'i18next';
import { Trans, withTranslation } from 'react-i18next';
import { Spacer } from '../../../components/helpers';
import { Trans, useTranslation } from 'react-i18next';
import { Spacer } from '../../../components/helpers';
import { isMicrosoftTranscriptLink } from '../../../../../utils/validate';
import {
linkMsUsername,
unlinkMsUsername,
@@ -27,7 +28,7 @@ import Login from '../../../components/Header/components/login';
import './link-ms-user.css';
const mapStateToProps = createSelector(
const mapState = createSelector(
isSignedInSelector,
msUsernameSelector,
isProcessingSelector,
@@ -42,7 +43,7 @@ const mapStateToProps = createSelector(
})
);
const mapDispatchToProps = (dispatch: Dispatch) =>
const mapDispatch = (dispatch: Dispatch) =>
bindActionCreators(
{
linkMsUsername,
@@ -52,26 +53,20 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
dispatch
);
interface LinkMsUserProps {
isSignedIn: boolean;
msUsername: string | undefined | null;
linkMsUsername: (arg0: { msTranscriptUrl: string }) => void;
unlinkMsUsername: () => void;
isProcessing: boolean;
setIsProcessing: (arg0: boolean) => void;
t: TFunction;
}
const connector = connect(mapState, mapDispatch);
export function LinkMsUser({
type Props = ConnectedProps<typeof connector>;
function LinkMsUser({
isSignedIn,
msUsername,
linkMsUsername,
unlinkMsUsername,
isProcessing,
setIsProcessing,
t
}: LinkMsUserProps): JSX.Element {
const [msTranscriptUrl, setMsTranscriptUrl] = useState<string>('');
setIsProcessing
}: Props): JSX.Element {
const { t } = useTranslation();
const [msTranscriptUrl, setMsTranscriptUrl] = useState('');
function handleLinkUsername(e: React.FormEvent) {
e.preventDefault();
@@ -84,6 +79,11 @@ export function LinkMsUser({
setMsTranscriptUrl(e.target.value);
}
const isValid = isMicrosoftTranscriptLink(msTranscriptUrl);
const isPristine = msTranscriptUrl === '';
const isDisabled = isProcessing || !isValid;
const showWarning = !isPristine && !isValid;
return !isSignedIn ? (
<>
<h2 className='link-ms-user-title'>{t('learn.ms.link-header')}</h2>
@@ -133,7 +133,7 @@ export function LinkMsUser({
<Spacer size='medium' />
<form onSubmit={handleLinkUsername}>
<FormGroup>
<FormGroup validationState={isValid ? 'success' : 'error'}>
<ControlLabel>
<strong>{t('learn.ms.transcript-label')}</strong>
</ControlLabel>
@@ -144,7 +144,7 @@ export function LinkMsUser({
/>
</FormGroup>
<Button
disabled={isProcessing || msTranscriptUrl.length === 0}
disabled={isDisabled}
block={true}
bsStyle='primary'
className='btn-invert'
@@ -152,6 +152,13 @@ export function LinkMsUser({
>
{t('buttons.link-account')}
</Button>
{showWarning && (
<HelpBlock>
<Trans i18nKey='learn.ms.invalid-transcript'>
placeholder <code>placeholder</code> placeholder
</Trans>
</HelpBlock>
)}
</form>
</div>
)}
@@ -161,7 +168,4 @@ export function LinkMsUser({
LinkMsUser.displayName = 'LinkMsUser';
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(LinkMsUser));
export default connector(LinkMsUser);
+59 -1
View File
@@ -3,7 +3,8 @@ import {
usernameTooShort,
validationSuccess,
usernameIsHttpStatusCode,
invalidCharError
invalidCharError,
isMicrosoftTranscriptLink
} from './validate';
function inRange(num: number, range: number[]) {
@@ -56,3 +57,60 @@ describe('isValidUsername', () => {
}
});
});
const baseUrl = 'https://learn.microsoft.com/';
describe('isMicrosoftTranscriptLink', () => {
it('should reject links to domains other than learn.microsoft.com', () => {
{
expect(isMicrosoftTranscriptLink('https://lean.microsoft.com')).toBe(
false
);
expect(isMicrosoftTranscriptLink('https://learn.microsft.com')).toBe(
false
);
}
});
it('should reject links without a username', () => {
expect(isMicrosoftTranscriptLink(`${baseUrl}/en-us/users/`)).toBe(false);
});
it('should reject links without a unique id', () => {
expect(
isMicrosoftTranscriptLink(`${baseUrl}/en-us/users/moT01/transcript`)
).toBe(false);
});
it('should reject links with anything after the unique id', () => {
expect(
isMicrosoftTranscriptLink(
`${baseUrl}/en-us/users/moT01/transcript/any-id/more-stuff`
)
).toBe(false);
});
it('should reject the placeholder link', () => {
expect(
isMicrosoftTranscriptLink(
'https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID'
)
).toBe(false);
expect(
isMicrosoftTranscriptLink(
'https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID/'
)
).toBe(false);
});
it.each(['en-us', 'fr-fr', 'lang-country'])(
'should accept links with the %s locale',
locale => {
expect(
isMicrosoftTranscriptLink(
`https://learn.microsoft.com/${locale}/users/moT01/transcript/any-id`
)
).toBe(true);
}
);
});
+20
View File
@@ -36,3 +36,23 @@ export const isValidUsername = (str: string): Validated => {
if (isHttpStatusCode(str)) return usernameIsHttpStatusCode;
return validationSuccess;
};
// link template:
// https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID
export const isMicrosoftTranscriptLink = (value: string): boolean => {
let url;
try {
url = new URL(value);
} catch {
return false;
}
const correctDomain = url.hostname === 'learn.microsoft.com';
const correctPath = !!url.pathname.match(
/^\/[^/]+\/users\/[^/]+\/transcript\/[^/]+$/
);
const notPlaceholder = !url.pathname.match(
'/LOCALE/users/USERNAME/transcript/ID'
);
return correctDomain && correctPath && notPlaceholder;
};