fix: Move content into to frontend directory

Doing this BEFORE the merge prevents a lot of merge conflicts.

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-11-11 11:16:18 +01:00
parent 4e18ce38f3
commit 762a0a850e
No known key found for this signature in database
GPG key ID: B97799103358209B
1051 changed files with 0 additions and 35 deletions

View file

@ -0,0 +1,68 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { BaseUrlFromEnvExtractor } from './base-url-from-env-extractor'
describe('BaseUrlFromEnvExtractor', () => {
it('should return the base urls if both are valid urls', () => {
process.env.HD_EDITOR_BASE_URL = 'https://editor.example.org/'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
const result = baseUrlFromEnvExtractor.extractBaseUrls()
expect(result.isPresent()).toBeTruthy()
expect(result.get()).toStrictEqual({
renderer: 'https://renderer.example.org/',
editor: 'https://editor.example.org/'
})
})
it('should return an empty optional if no var is set', () => {
process.env.HD_EDITOR_BASE_URL = undefined
process.env.HD_RENDERER_BASE_URL = undefined
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
})
it("should return an empty optional if editor base url isn't an URL", () => {
process.env.HD_EDITOR_BASE_URL = 'bibedibabedibu'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
})
it("should return an empty optional if renderer base url isn't an URL", () => {
process.env.HD_EDITOR_BASE_URL = 'https://editor.example.org/'
process.env.HD_RENDERER_BASE_URL = 'bibedibabedibu'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
})
it("should return an empty optional if editor base url isn't ending with a slash", () => {
process.env.HD_EDITOR_BASE_URL = 'https://editor.example.org'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
})
it("should return an empty optional if renderer base url isn't ending with a slash", () => {
process.env.HD_EDITOR_BASE_URL = 'https://editor.example.org/'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
})
it('should copy editor base url to renderer base url if renderer base url is omitted', () => {
process.env.HD_EDITOR_BASE_URL = 'https://editor.example.org/'
delete process.env.HD_RENDERER_BASE_URL
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
const result = baseUrlFromEnvExtractor.extractBaseUrls()
expect(result.isPresent()).toBeTruthy()
expect(result.get()).toStrictEqual({
renderer: 'https://editor.example.org/',
editor: 'https://editor.example.org/'
})
})
})

View file

@ -0,0 +1,89 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Optional } from '@mrdrogdrog/optional'
import type { BaseUrls } from '../components/common/base-url/base-url-context-provider'
import { Logger } from './logger'
import { isTestMode } from './test-modes'
/**
* Extracts the editor and renderer base urls from the environment variables.
*/
export class BaseUrlFromEnvExtractor {
private baseUrls: Optional<BaseUrls> | undefined
private logger = new Logger('Base URL Configuration')
private extractUrlFromEnvVar(envVarName: string, envVarValue: string | undefined): Optional<URL> {
return Optional.ofNullable(envVarValue)
.filter((value) => {
const endsWithSlash = value.endsWith('/')
if (!endsWithSlash) {
this.logger.error(`${envVarName} must end with an '/'`)
}
return endsWithSlash
})
.map((value) => {
try {
return new URL(value)
} catch (error) {
return null
}
})
}
private extractEditorBaseUrlFromEnv(): Optional<URL> {
const envValue = this.extractUrlFromEnvVar('HD_EDITOR_BASE_URL', process.env.HD_EDITOR_BASE_URL)
if (envValue.isEmpty()) {
this.logger.error("HD_EDITOR_BASE_URL isn't a valid URL!")
}
return envValue
}
private extractRendererBaseUrlFromEnv(editorBaseUrl: URL): Optional<URL> {
if (isTestMode) {
this.logger.info('Test mode activated. Using editor base url for renderer.')
return Optional.of(editorBaseUrl)
}
if (!process.env.HD_RENDERER_BASE_URL) {
this.logger.info('HD_RENDERER_BASE_URL is unset. Using editor base url for renderer.')
return Optional.of(editorBaseUrl)
}
return this.extractUrlFromEnvVar('HD_RENDERER_BASE_URL', process.env.HD_RENDERER_BASE_URL)
}
private renewBaseUrls(): void {
this.baseUrls = this.extractEditorBaseUrlFromEnv().flatMap((editorBaseUrl) =>
this.extractRendererBaseUrlFromEnv(editorBaseUrl).map((rendererBaseUrl) => {
return {
editor: editorBaseUrl.toString(),
renderer: rendererBaseUrl.toString()
}
})
)
this.baseUrls.ifPresent((urls) => {
this.logger.info('Editor base URL', urls.editor.toString())
this.logger.info('Renderer base URL', urls.renderer.toString())
})
}
private isEnvironmentExtractDone(): boolean {
return this.baseUrls !== undefined
}
/**
* Extracts the editor and renderer base urls from the environment variables.
*
* @return An {@link Optional} with the base urls.
*/
public extractBaseUrls(): Optional<BaseUrls> {
if (!this.isEnvironmentExtractDone()) {
this.renewBaseUrls()
}
return Optional.ofNullable(this.baseUrls).flatMap((value) => value)
}
}

View file

@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { isTestMode } from './test-modes'
export interface PropsWithDataCypressId {
'data-cypress-id'?: string | undefined
}
/**
* Returns an object with the "data-cypress-id" attribute that is used to find
* elements for integration tests.
* This works only if the runtime is built in test mode.
*
* @param identifier The identifier that is used to find the element
* @return An object if in test mode, {@link undefined} otherwise.
*/
export const cypressId = (identifier: string | undefined | PropsWithDataCypressId): PropsWithDataCypressId => {
if (!isTestMode || !identifier) {
return {}
}
const attributeContent = typeof identifier === 'string' ? identifier : identifier['data-cypress-id']
return attributeContent !== undefined ? { 'data-cypress-id': attributeContent } : {}
}
/**
* Returns an object with an attribute that starts with "data-cypress-" and the given attribute name.
* It is used to check additional data during integration tests.
* This works only if the runtime is built in test mode.
*
* @param attribute The attribute name
* @param value The attribute content
* @return An object if in test mode, undefined otherwise.
*/
export const cypressAttribute = (
attribute: string,
value: string | undefined
): Record<string, string | undefined> | undefined => {
if (!isTestMode) {
return
}
return {
[`data-cypress-${attribute}`]: value
}
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Detects if the application is running on client side.
*/
export const isClientSideRendering = (): boolean => {
return typeof window !== 'undefined' && typeof window.navigator !== 'undefined'
}

View file

@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Logger, LogLevel } from './logger'
import { Settings } from 'luxon'
describe('Logger', () => {
let consoleMock: jest.SpyInstance
let originalNow: () => number
let dateShift = 0
function mockConsole(methodToMock: LogLevel, onResult: (result: string) => void) {
consoleMock = jest.spyOn(console, methodToMock).mockImplementation((...data: string[]) => {
const result = data.reduce((state, current) => state + ' ' + current)
onResult(result)
})
}
beforeEach(() => {
originalNow = Settings.now
Settings.now = () => new Date(2021, 9, 25, dateShift, 1 + dateShift, 2 + dateShift, 3 + dateShift).valueOf()
})
afterEach(() => {
Settings.now = originalNow
consoleMock.mockReset()
})
it('logs a debug message into the console', (done) => {
dateShift = 0
mockConsole(LogLevel.DEBUG, (result) => {
expect(consoleMock).toBeCalled()
expect(result).toEqual('%c[2021-10-25 00:01:02] %c(prefix) color: yellow color: orange beans')
done()
})
new Logger('prefix').debug('beans')
})
it('logs a info message into the console', (done) => {
dateShift = 1
mockConsole(LogLevel.INFO, (result) => {
expect(consoleMock).toBeCalled()
expect(result).toEqual('%c[2021-10-25 01:02:03] %c(prefix) color: yellow color: orange toast')
done()
})
new Logger('prefix').info('toast')
})
it('logs a warn message into the console', (done) => {
dateShift = 2
mockConsole(LogLevel.WARN, (result) => {
expect(consoleMock).toBeCalled()
expect(result).toEqual('%c[2021-10-25 02:03:04] %c(prefix) color: yellow color: orange eggs')
done()
})
new Logger('prefix').warn('eggs')
})
it('logs a error message into the console', (done) => {
dateShift = 3
mockConsole(LogLevel.ERROR, (result) => {
expect(consoleMock).toBeCalled()
expect(result).toEqual('%c[2021-10-25 03:04:05] %c(prefix) color: yellow color: orange bacon')
done()
})
new Logger('prefix').error('bacon')
})
})

View file

@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { DateTime } from 'luxon'
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error'
}
type OutputFunction = (...data: unknown[]) => void
/**
* Simple logger that prefixes messages with a timestamp and a name.
*/
export class Logger {
private readonly scope: string
constructor(scope: string) {
this.scope = scope
}
/**
* Logs a debug message.
*
* @param data data to log
*/
debug(...data: unknown[]): void {
this.log(LogLevel.DEBUG, ...data)
}
/**
* Logs a normal informative message.
*
* @param data data to log
*/
info(...data: unknown[]): void {
this.log(LogLevel.INFO, ...data)
}
/**
* Logs a warning.
*
* @param data data to log
*/
warn(...data: unknown[]): void {
this.log(LogLevel.WARN, ...data)
}
/**
* Logs an error.
*
* @param data data to log
*/
error(...data: unknown[]): void {
this.log(LogLevel.ERROR, ...data)
}
private log(loglevel: LogLevel, ...data: unknown[]) {
const preparedData = [...this.prefix(), ...data]
const logOutput = Logger.getLogOutput(loglevel)
logOutput(...preparedData)
}
private static getLogOutput(logLevel: LogLevel): OutputFunction {
switch (logLevel) {
case LogLevel.INFO:
return console.info
case LogLevel.DEBUG:
return console.debug
case LogLevel.ERROR:
return console.error
case LogLevel.WARN:
return console.warn
}
}
private prefix(): string[] {
const timestamp = DateTime.now().toFormat('yyyy-MM-dd HH:mm:ss')
return [`%c[${timestamp}] %c(${this.scope})`, 'color: yellow', 'color: orange']
}
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { FileContentFormat, readFile } from './read-file'
describe('read file', () => {
it('reads files as text', async () => {
const a = await readFile(new Blob(['Kinderriegel'], { type: 'text/plain' }), FileContentFormat.TEXT)
expect(a).toBe('Kinderriegel')
})
it('reads files as data url', async () => {
const a = await readFile(new Blob(['Kinderriegel'], { type: 'text/plain' }), FileContentFormat.DATA_URL)
expect(a).toBe('data:text/plain;base64,S2luZGVycmllZ2Vs')
})
})

View file

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export enum FileContentFormat {
TEXT,
DATA_URL
}
/**
* Reads the given {@link File}.
*
* @param file The file to read
* @param fileReaderMode Defines as what the file content should be formatted.
* @throws {Error} if an invalid read mode was given or if the file couldn't be read.
* @return the file content
*/
export const readFile = async (file: Blob, fileReaderMode: FileContentFormat): Promise<string> => {
return new Promise<string>((resolve, reject) => {
const fileReader = new FileReader()
fileReader.addEventListener('load', () => {
resolve(fileReader.result as string)
})
fileReader.addEventListener('error', (error) => {
reject(error)
})
switch (fileReaderMode) {
case FileContentFormat.DATA_URL:
fileReader.readAsDataURL(file)
break
case FileContentFormat.TEXT:
fileReader.readAsText(file)
break
default:
throw new Error('Unknown file reader mode')
}
})
}

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface PropsWithDataTestId {
'data-testid'?: string | undefined
}
/**
* Returns an object with the "data-testid" attribute that is used to find
* elements in unit tests.
* This works only if the runtime is built in test mode.
*
* @param identifier The identifier that is used to find the element
* @return An object if in test mode, undefined otherwise.
*/
export const testId = (identifier: string): PropsWithDataTestId => {
return process.env.NODE_ENV === 'test' ? { 'data-testid': identifier } : {}
}

View file

@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* This file is intentionally a js and not a ts file because it is used in `next.config.js`
*/
/**
* Checks if the given string is a positive answer (yes, true or 1).
*
* @param {string} value The value to check
* @return {boolean} {@code true} if the value describes a positive answer string
*/
const isPositiveAnswer = (value) => {
const lowerValue = value.toLowerCase()
return lowerValue === 'yes' || lowerValue === '1' || lowerValue === 'true'
}
/**
* Defines if the current runtime is built in e2e test mode.
* @type boolean
*/
const isTestMode = !!process.env.NEXT_PUBLIC_TEST_MODE && isPositiveAnswer(process.env.NEXT_PUBLIC_TEST_MODE)
/**
* Defines if the current runtime should use the mocked backend.
* @type boolean
*/
const isMockMode = !!process.env.NEXT_PUBLIC_USE_MOCK_API && isPositiveAnswer(process.env.NEXT_PUBLIC_USE_MOCK_API)
/**
* Defines if the current runtime was built in development mode.
* @type boolean
*/
const isDevMode = process.env.NODE_ENV === 'development'
module.exports = {
isTestMode,
isMockMode,
isDevMode
}

View file

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useMemo } from 'react'
import type { PropsWithChildren } from 'react'
import { isClientSideRendering } from './is-client-side-rendering'
import { useBaseUrl } from '../hooks/common/use-base-url'
/**
* Checks if the url of the current browser window matches the expected origin.
* This is necessary to ensure that the render endpoint is only opened from the rendering origin.
*
* @param children The children react element that should be rendered if the origin is correct
*/
export const ExpectedOriginBoundary: React.FC<PropsWithChildren> = ({ children }) => {
const baseUrl = useBaseUrl()
const expectedOrigin = useMemo(() => new URL(baseUrl).origin, [baseUrl])
if (isClientSideRendering() && window.location.origin !== expectedOrigin) {
return <span>{`You can't open this page using this URL. For this endpoint "${expectedOrigin}" is expected.`}</span>
} else {
return <Fragment>{children}</Fragment>
}
}

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Waits until all other pending promises are processed.
*
* NodeJS has a queue for async code that waits for being processed. This method adds a promise to the very end of this queue.
* If the promise is resolved then this means that all other promises before it have been processed as well.
*
* @return A promise which resolves when all other promises have been processed
*/
export function waitForOtherPromisesToFinish(): Promise<void> {
return new Promise((resolve) => process.nextTick(resolve))
}