mirror of
https://github.com/outline/outline.git
synced 2026-08-03 13:27:25 +03:00
fix: urls.unfurl does not correctly treat as internal on FQD (#13189)
This commit is contained in:
+19
-1
@@ -28,7 +28,11 @@ import { isEmail } from "validator";
|
||||
import { TeamPreferenceDefaults } from "@shared/constants";
|
||||
import type { TeamPreferences } from "@shared/types";
|
||||
import { TeamPreference, UserRole } from "@shared/types";
|
||||
import { getBaseDomain, RESERVED_SUBDOMAINS } from "@shared/utils/domains";
|
||||
import {
|
||||
getBaseDomain,
|
||||
parseDomain,
|
||||
RESERVED_SUBDOMAINS,
|
||||
} from "@shared/utils/domains";
|
||||
import { attachmentRedirectRegex } from "@shared/utils/ProsemirrorHelper";
|
||||
import { parseEmail } from "@shared/utils/email";
|
||||
import { TeamValidation } from "@shared/validations";
|
||||
@@ -285,6 +289,20 @@ class Team extends ParanoidModel<
|
||||
return url.href.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given url points at this team's installation, taking
|
||||
* into account custom domains and hosted subdomains.
|
||||
*
|
||||
* @param url The url to check.
|
||||
* @returns True if the url belongs to this team.
|
||||
*/
|
||||
public isTeamUrl(url: string): boolean {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
return parseDomain(url).host === parseDomain(this.url).host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a code that can be used to delete the user's team. The code will
|
||||
* be rotated when the user signs out.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Mock } from "vitest";
|
||||
import { randomString } from "@shared/random";
|
||||
import { UnfurlResourceType } from "@shared/types";
|
||||
import env from "@server/env";
|
||||
import type { User } from "@server/models";
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
buildCollection,
|
||||
buildDocument,
|
||||
buildShare,
|
||||
buildTeam,
|
||||
buildUser,
|
||||
} from "@server/test/factories";
|
||||
import { getTestServer } from "@server/test/support";
|
||||
@@ -207,6 +209,74 @@ Usage instructions here.`,
|
||||
expect(body.summary).toEqual(
|
||||
`## Installation
|
||||
|
||||
Install instructions here.`
|
||||
);
|
||||
});
|
||||
|
||||
it("should succeed when document url is on the team custom domain", async () => {
|
||||
const team = await buildTeam({
|
||||
domain: `${randomString(10)}.example.com`,
|
||||
});
|
||||
const teamUser = await buildUser({ teamId: team.id });
|
||||
const document = await buildDocument({
|
||||
teamId: team.id,
|
||||
userId: teamUser.id,
|
||||
text: `Intro paragraph.
|
||||
|
||||
## Installation
|
||||
|
||||
Install instructions here.`,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/urls.unfurl", teamUser, {
|
||||
body: {
|
||||
url: `${team.url}/${document.url}#h-installation`,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.type).toEqual(UnfurlResourceType.Document);
|
||||
expect(body.id).toEqual(document.id);
|
||||
expect(body.summary).toEqual(
|
||||
`## Installation
|
||||
|
||||
Install instructions here.`
|
||||
);
|
||||
});
|
||||
|
||||
it("should succeed when document url is on the team subdomain", async () => {
|
||||
vi.spyOn(env, "isCloudHosted", "get").mockReturnValue(true);
|
||||
|
||||
const team = await buildTeam({
|
||||
subdomain: randomString({
|
||||
length: 10,
|
||||
charset: "alphabetic",
|
||||
capitalization: "lowercase",
|
||||
}),
|
||||
});
|
||||
const teamUser = await buildUser({ teamId: team.id });
|
||||
const document = await buildDocument({
|
||||
teamId: team.id,
|
||||
userId: teamUser.id,
|
||||
text: `Intro paragraph.
|
||||
|
||||
## Installation
|
||||
|
||||
Install instructions here.`,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/urls.unfurl", teamUser, {
|
||||
body: {
|
||||
url: `${team.url}/${document.url}#h-installation`,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.type).toEqual(UnfurlResourceType.Document);
|
||||
expect(body.id).toEqual(document.id);
|
||||
expect(body.summary).toEqual(
|
||||
`## Installation
|
||||
|
||||
Install instructions here.`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import Router from "koa-router";
|
||||
import { traceFunction } from "@server/logging/tracing";
|
||||
import isUUID from "validator/lib/isUUID";
|
||||
import { MentionType, UnfurlResourceType } from "@shared/types";
|
||||
import { getBaseDomain, parseDomain } from "@shared/utils/domains";
|
||||
import { getBaseDomain } from "@shared/utils/domains";
|
||||
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
|
||||
import parseMentionUrl from "@shared/utils/parseMentionUrl";
|
||||
import { isInternalUrl, parseShareIdFromUrl } from "@shared/utils/urls";
|
||||
@@ -45,8 +45,13 @@ router.post(
|
||||
const { url, documentId } = ctx.input.body;
|
||||
const urlObj = new URL(url);
|
||||
|
||||
// A url may point at this installation without matching the default env.URL
|
||||
// when the team is on a custom domain or hosted subdomain.
|
||||
const isTeamUrl =
|
||||
isInternalUrl(url) || !!ctx.state.auth.user?.team.isTeamUrl(url);
|
||||
|
||||
// Public share URLs – does not require authentication
|
||||
if (isInternalUrl(url)) {
|
||||
if (isTeamUrl) {
|
||||
const shareId = parseShareIdFromUrl(url);
|
||||
|
||||
if (shareId) {
|
||||
@@ -159,7 +164,7 @@ router.post(
|
||||
}
|
||||
|
||||
// Internal resources
|
||||
if (isInternalUrl(url) || parseDomain(url).host === actor.team.domain) {
|
||||
if (isTeamUrl) {
|
||||
const previewDocumentId = parseDocumentSlug(url);
|
||||
if (previewDocumentId) {
|
||||
const document = await Document.findByPk(previewDocumentId, {
|
||||
|
||||
Reference in New Issue
Block a user