Skip to content
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
17 changes: 17 additions & 0 deletions .github/workflows/webapp-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ jobs:
run: |
npm run i18n-extract:check

check-external-links:
needs: check-lint
runs-on: ubuntu-24.04
timeout-minutes: 15
defaults:
run:
working-directory: webapp
steps:
- name: ci/checkout-repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: ci/setup
uses: ./.github/actions/webapp-setup
- name: ci/check-external-links
run: |
set -o pipefail
npm run check-external-links -- --markdown | tee -a $GITHUB_STEP_SUMMARY

check-types:
needs: check-lint
runs-on: ubuntu-24.04
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3261,7 +3261,7 @@ const AdminDefinition: AdminDefinitionType = {
featureName: 'burn_on_read',
title: defineMessage({id: 'admin.burn_on_read_feature_discovery.title', defaultMessage: 'Send burn-on-read messages that are automatically deleted after being read'}),
description: defineMessage({id: 'admin.burn_on_read_feature_discovery.description', defaultMessage: 'With Mattermost Enterprise Advanced, users can send transient messages that are automatically deleted a fixed time after they are read by a recipient.'}),
learnMoreURL: 'https://docs.mattermost.com/deployment/burn-on-read-messages.html',
learnMoreURL: 'https://docs.mattermost.com/end-user-guide/collaborate/send-messages.html#send-burn-on-read-messages',
svgImage: BurnOnReadSVG,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const AttributeBasedAccessControlFeatureDiscovery: React.FC = () => {
id: 'admin.attribute_based_access_control_feature_discovery.desc',
defaultMessage: 'Create policies containing access rules based on user attributes and apply them to channels and other resources within Mattermost.',
})}
learnMoreURL='https://docs.mattermost.com/deployment/'
learnMoreURL='https://docs.mattermost.com/administration-guide/manage/admin/attribute-based-access-control.html'
featureDiscoveryImage={
<SystemRolesSVG
width={294}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,31 @@ describe('useExternalLink', () => {
expect(firstHref).toBe(secondHref);
expect(firstParams).toBe(secondParams);
});

it('do not substitute %20 on query params', () => {
const url = 'https://www.mattermost.com/some/url?subject=hello%20world';
const {result: {current: [href]}} = renderHookWithContext(() => useExternalLink(url), getBaseState());
expect(href).toContain('subject=hello%20world');
});

it('do not error on invalid URLs', () => {
const invalidUrl = 'not a valid url';
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(invalidUrl), getBaseState());
expect(href).toBe(invalidUrl);
expect(queryParams).toEqual({});
});

it('do not modify arbitrary links that happen to include mattermost.com', () => {
const invalidUrl = 'https://example.com/mattermost.com';
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(invalidUrl), getBaseState());
expect(href).toBe(invalidUrl);
expect(queryParams).toEqual({});
});

it('do not modify mailto links on mattermost.com', () => {
const mailtoUrl = 'mailto:support@mattermost.com';
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(mailtoUrl), getBaseState());
expect(href).toBe(mailtoUrl);
expect(queryParams).toEqual({});
});
});
24 changes: 22 additions & 2 deletions webapp/channels/src/components/common/hooks/use_external_link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ export type ExternalLinkQueryParams = {
userId?: string;
}

/**
* useExternalLink is used when linking outside of the MM server to add extra tracking parameters when linking to any
* page on mattermost.com (such as our docs or marketing websites). When passed any URL that isn't on mattermost.com,
* it returns the original URL unmodified.
*
* @param href The external URL being linked to
* @param location The location of the link within the app
* @param overwriteQueryParams
* @return {[string, Record<string, string>]} A tuple containing the URL (whether or not it was modified) and all query
* parameters on that link (either pre-existing or added by this hook)
*/
export function useExternalLink(href: string, location: string = '', overwriteQueryParams: ExternalLinkQueryParams = {}): [string, Record<string, string>] {
const userId = useSelector(getCurrentUserId);
const config = useSelector(getConfig);
Expand All @@ -28,11 +39,20 @@ export function useExternalLink(href: string, location: string = '', overwriteQu
const isCloud = useSelector((state: GlobalState) => getLicense(state)?.Cloud === 'true');

return useMemo(() => {
if (!href?.includes('mattermost.com') || href?.startsWith('mailto:')) {
let parsedUrl;
try {
parsedUrl = new URL(href);
} catch {
return [href, {}];
}

const parsedUrl = new URL(href);
if (parsedUrl.hostname !== 'mattermost.com' && !parsedUrl.hostname.endsWith('.mattermost.com')) {
return [href, {}];
}

if (parsedUrl.protocol === 'mailto:') {
return [href, {}];
}

// Determine edition type (enterprise vs team)
const isEnterpriseReady = config?.BuildEnterpriseReady === 'true';
Expand Down
2 changes: 1 addition & 1 deletion webapp/channels/src/components/help/commands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const HelpCommands = (): JSX.Element => {
values={{
link: (chunks: React.ReactNode) => (
<ExternalLink
href='https://docs.mattermost.com/integrations/slash-commands-built-in.html'
href='https://docs.mattermost.com/integrations-guide/built-in-slash-commands.html'
location='help_commands'
>
{chunks}
Expand Down
4 changes: 2 additions & 2 deletions webapp/channels/src/utils/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,7 @@ export const AboutLinks = {
};

export const CloudLinks = {
BILLING_DOCS: 'https://docs.mattermost.com/pl/cloud-billing',
BILLING_DOCS: 'https://docs.mattermost.com/product-overview/cloud-subscriptions.html',
PRICING: 'https://mattermost.com/pl/pricing/',
PRORATED_PAYMENT: 'https://mattermost.com/pl/mattermost-cloud-prorate-documentation',
DEPLOYMENT_OPTIONS: 'https://mattermost.com/deploy/',
Expand Down Expand Up @@ -1074,7 +1074,7 @@ export const DocLinks = {
SETUP_LDAP: 'https://mattermost.com/pl/setup-ldap',
SETUP_PERFORMANCE_MONITORING: 'https://mattermost.com/pl/setup-performance-monitoring',
SETUP_PUSH_NOTIFICATIONS: 'https://mattermost.com/pl/setup-push-notifications',
SETUP_SAML: 'https://docs.mattermost.com/pl/setup-saml',
SETUP_SAML: 'https://docs.mattermost.com/administration-guide/onboard/sso-saml.html',
SHARE_LINKS_TO_MESSAGES: 'https://mattermost.com/pl/share-links-to-messages',
SITE_URL: 'https://mattermost.com/pl/configure-site-url',
SSL_CERTIFICATE: 'https://mattermost.com/pl/setup-ssl-client-certificate',
Expand Down
3 changes: 2 additions & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"i18n-extract": "npm run i18n-extract --workspaces --if-present",
"i18n-extract:check": "npm run i18n-extract:check --workspaces --if-present",
"clean": "npm run clean --workspaces --if-present && rm -rf node_modules .parcel-cache",
"gen-lang-imports": "node scripts/gen_lang_imports.mjs"
"gen-lang-imports": "node scripts/gen_lang_imports.mjs",
"check-external-links": "node scripts/check-external-links.mjs"
},
"dependencies": {
"@mattermost/compass-icons": "0.1.53",
Expand Down
Loading
Loading