Restructures + New Evironment Variables (#1230)

* Use document base uri for react router

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Rename getAndSetUser to fetchAndSetUser

Getter should be reserved for simple get functions.
Everything that does a bit more logic should use a more meaningful verb.

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Rename getFrontPageContent to fetchFrontPageContent

Getter should be reserved for simple get functions.
Everything that does a bit more logic should use a more meaningful verb.

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Reformat code

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Use PUBLIC_URL env var in index.html

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Introduce new environment variables

For better testing (especially if you have multiple endpoints) this commit introduces
REACT_APP_BACKEND_BASE_URL, REACT_APP_FRONTEND_ASSETS_URL and REACT_APP_CUSTOMIZE_ASSETS_URL

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Remove redundant license information

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Remove redundant license information

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Fix rebase issues

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Remove unused file

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Correct parameter

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Fix run tasks

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Force use of bash

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Fix link to cypress picture

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* revert change

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* fix url

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Remove license info

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Revert rebase issues

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Add missing banner code

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Fix test url

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Useless change to trigger github

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Don't set backend base url because this break the mock mode detection

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

Co-authored-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Tilman Vatteroth 2021-05-02 22:38:43 +02:00 committed by GitHub
parent 9cf7980334
commit 2c5a03b3ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 193 additions and 162 deletions

View file

@ -1,21 +1,24 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Suspense, useCallback, useEffect, useState } from 'react'
import { useFrontendBaseUrl } from '../../hooks/common/use-frontend-base-url'
import { useBackendBaseUrl } from '../../hooks/common/use-backend-base-url'
import './application-loader.scss'
import { createSetUpTaskList, InitTask } from './initializers'
import { LoadingScreen } from './loading-screen'
import { useCustomizeAssetsUrl } from '../../hooks/common/use-customize-assets-url'
import { useFrontendAssetsUrl } from '../../hooks/common/use-frontend-assets-url'
export const ApplicationLoader: React.FC = ({ children }) => {
const frontendUrl = useFrontendBaseUrl()
const frontendAssetsUrl = useFrontendAssetsUrl()
const backendBaseUrl = useBackendBaseUrl()
const customizeAssetsUrl = useCustomizeAssetsUrl()
const setUpTasks = useCallback(() => {
return createSetUpTaskList(frontendUrl)
}, [frontendUrl])
const setUpTasks = useCallback(() => createSetUpTaskList(frontendAssetsUrl, customizeAssetsUrl, backendBaseUrl),
[backendBaseUrl, customizeAssetsUrl, frontendAssetsUrl])
const [failedTitle, setFailedTitle] = useState<string>('')
const [doneTasks, setDoneTasks] = useState<number>(0)

View file

@ -5,16 +5,10 @@
*/
import { getConfig } from '../../../api/config'
import { setApiUrl } from '../../../redux/api-url/methods'
import { setBanner } from '../../../redux/banner/methods'
import { setConfig } from '../../../redux/config/methods'
import { getAndSetUser } from '../../login-page/auth/utils'
export const loadAllConfig: (baseUrl: string) => Promise<void> = async (baseUrl) => {
setApiUrl({
apiUrl: (process.env.REACT_APP_BACKEND || baseUrl) + '/api/private'
})
export const fetchFrontendConfig = async (): Promise<void> => {
const config = await getConfig()
if (!config) {
return Promise.reject(new Error('Config empty!'))
@ -29,6 +23,4 @@ export const loadAllConfig: (baseUrl: string) => Promise<void> = async (baseUrl)
show: banner.text !== '' && banner.timestamp !== lastAcknowledgedTimestamp
})
}
await getAndSetUser()
}

View file

@ -10,7 +10,7 @@ import Backend from 'i18next-http-backend'
import { Settings } from 'luxon'
import { initReactI18next } from 'react-i18next'
export const setUpI18n = async (): Promise<void> => {
export const setUpI18n = async (frontendAssetsUrl: string): Promise<void> => {
await i18n
.use(Backend)
.use(LanguageDetector)
@ -19,7 +19,7 @@ export const setUpI18n = async (): Promise<void> => {
fallbackLng: 'en',
debug: process.env.NODE_ENV !== 'production',
backend: {
loadPath: '/locales/{{lng}}.json'
loadPath: `${ frontendAssetsUrl }/locales/{{lng}}.json`
},
interpolation: {

View file

@ -4,9 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { loadAllConfig } from './configLoader'
import { setUpI18n } from './i18n'
import { refreshHistoryState } from '../../../redux/history/methods'
import { setApiUrl } from '../../../redux/api-url/methods'
import { fetchAndSetUser } from '../../login-page/auth/utils'
import { fetchFrontendConfig } from './fetch-frontend-config'
const customDelay: () => Promise<void> = async () => {
if (window.localStorage.getItem('customDelay')) {
@ -21,13 +23,20 @@ export interface InitTask {
task: Promise<void>
}
export const createSetUpTaskList = (baseUrl: string): InitTask[] => {
export const createSetUpTaskList = (frontendAssetsUrl: string, customizeAssetsUrl: string, backendBaseUrl: string): InitTask[] => {
setApiUrl({
apiUrl: `${ backendBaseUrl }/api/private`
})
return [{
name: 'Load Translations',
task: setUpI18n()
task: setUpI18n(frontendAssetsUrl)
}, {
name: 'Load config',
task: loadAllConfig(baseUrl)
task: fetchFrontendConfig()
}, {
name: 'Fetch user information',
task: fetchAndSetUser()
}, {
name: 'Load history state',
task: refreshHistoryState()

View file

@ -6,8 +6,8 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getFrontPageContent } from '../requests'
import { useFrontendBaseUrl } from '../../../hooks/common/use-frontend-base-url'
import { fetchFrontPageContent } from '../requests'
import { useCustomizeAssetsUrl } from '../../../hooks/common/use-customize-assets-url'
const MARKDOWN_WHILE_LOADING = ':zzz: {message}'
const MARKDOWN_IF_ERROR = ':::danger\n' +
@ -17,13 +17,13 @@ const MARKDOWN_IF_ERROR = ':::danger\n' +
export const useIntroPageContent = (): string => {
const { t } = useTranslation()
const [content, setContent] = useState<string>(() => MARKDOWN_WHILE_LOADING.replace('{message}', t('landing.intro.markdownWhileLoading')))
const frontendBaseUrl = useFrontendBaseUrl()
const customizeAssetsUrl = useCustomizeAssetsUrl()
useEffect(() => {
getFrontPageContent(frontendBaseUrl)
fetchFrontPageContent(customizeAssetsUrl)
.then((content) => setContent(content))
.catch(() => setContent(MARKDOWN_IF_ERROR.replace('{message}', t('landing.intro.markdownLoadingError'))))
}, [frontendBaseUrl, t])
}, [customizeAssetsUrl, t])
return content
}

View file

@ -6,8 +6,8 @@
import { defaultFetchConfig, expectResponseCode } from '../../api/utils'
export const getFrontPageContent = async (baseUrl: string): Promise<string> => {
const response = await fetch(baseUrl + '/intro.md', {
export const fetchFrontPageContent = async (customizeAssetsUrl: string): Promise<string> => {
const response = await fetch(customizeAssetsUrl + '/intro.md', {
...defaultFetchConfig,
method: 'GET'
})

View file

@ -26,7 +26,7 @@ export const PoweredByLinks: React.FC = () => {
<ExternalLink href={ links.webpage } text="HedgeDoc"/>
</Trans>
&nbsp;|&nbsp;
<TranslatedInternalLink href='/n/release-notes' i18nKey='landing.footer.releases'/>
<TranslatedInternalLink href="/n/release-notes" i18nKey="landing.footer.releases"/>
{
specialUrls.map(([i18nKey, href]) =>
<Fragment key={ i18nKey }>

View file

@ -7,7 +7,7 @@
import { getMe } from '../../../api/me'
import { setUser } from '../../../redux/user/methods'
export const getAndSetUser: () => (Promise<void>) = async () => {
export const fetchAndSetUser: () => (Promise<void>) = async () => {
const me = await getMe()
setUser({
id: me.id,

View file

@ -12,7 +12,7 @@ import { Link } from 'react-router-dom'
import { doInternalLogin } from '../../../api/auth'
import { ApplicationState } from '../../../redux'
import { ShowIf } from '../../common/show-if/show-if'
import { getAndSetUser } from './utils'
import { fetchAndSetUser } from './utils'
export const ViaInternal: React.FC = () => {
const { t } = useTranslation()
@ -23,7 +23,7 @@ export const ViaInternal: React.FC = () => {
const onLoginSubmit = useCallback((event: FormEvent) => {
doInternalLogin(username, password)
.then(() => getAndSetUser())
.then(() => fetchAndSetUser())
.catch(() => setError(true))
event.preventDefault()
}, [username, password])

View file

@ -1,7 +1,7 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { FormEvent, useCallback, useState } from 'react'
@ -11,7 +11,7 @@ import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { doLdapLogin } from '../../../api/auth'
import { ApplicationState } from '../../../redux'
import { getAndSetUser } from './utils'
import { fetchAndSetUser } from './utils'
export const ViaLdap: React.FC = () => {
const { t } = useTranslation()
@ -25,7 +25,7 @@ export const ViaLdap: React.FC = () => {
const onLoginSubmit = useCallback((event: FormEvent) => {
doLdapLogin(username, password)
.then(() => getAndSetUser())
.then(() => fetchAndSetUser())
.catch(() => setError(true))
event.preventDefault()
}, [username, password])

View file

@ -1,14 +1,14 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { FormEvent, useState } from 'react'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { doOpenIdLogin } from '../../../api/auth'
import { getAndSetUser } from './utils'
import { fetchAndSetUser } from './utils'
export const ViaOpenId: React.FC = () => {
useTranslation()
@ -16,7 +16,7 @@ export const ViaOpenId: React.FC = () => {
const [error, setError] = useState(false)
const doAsyncLogin: (() => Promise<void>) = async () => {
await doOpenIdLogin(openId)
await getAndSetUser()
await fetchAndSetUser()
}
const onFormSubmit = (event: FormEvent) => {

View file

@ -7,6 +7,7 @@
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { Alert } from 'react-bootstrap'
import { ShowIf } from '../../../common/show-if/show-if'
import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url'
export interface GraphvizFrameProps {
code: string
@ -26,6 +27,8 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
.forEach(child => child.remove())
}, [])
const frontendBaseUrl = useFrontendBaseUrl()
useEffect(() => {
if (!container.current) {
return
@ -34,7 +37,7 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
import(/* webpackChunkName: "d3-graphviz" */'@hpcc-js/wasm')
.then((wasmPlugin) => {
wasmPlugin.wasmFolder('/static/js')
wasmPlugin.wasmFolder(`${ frontendBaseUrl }/static/js`)
})
.then(() => import(/* webpackChunkName: "d3-graphviz" */ 'd3-graphviz'))
.then((graphvizImport) => {
@ -53,7 +56,7 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
.catch(() => {
console.error('error while loading graphviz')
})
}, [code, error, showError])
}, [code, error, frontendBaseUrl, showError])
return (
<Fragment>

View file

@ -1,7 +1,7 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react'
@ -10,7 +10,7 @@ import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { updateDisplayName } from '../../../api/me'
import { ApplicationState } from '../../../redux'
import { getAndSetUser } from '../../login-page/auth/utils'
import { fetchAndSetUser } from '../../login-page/auth/utils'
export const ProfileDisplayName: React.FC = () => {
const regexInvalidDisplayName = /^\s*$/
@ -37,7 +37,7 @@ export const ProfileDisplayName: React.FC = () => {
const doAsyncChange = async () => {
await updateDisplayName(displayName)
await getAndSetUser()
await fetchAndSetUser()
}
const changeNameSubmit = (event: FormEvent) => {

View file

@ -1,7 +1,7 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { FormEvent, Fragment, useCallback, useEffect, useState } from 'react'
@ -13,8 +13,7 @@ import { doInternalRegister } from '../../api/auth'
import { ApplicationState } from '../../redux'
import { TranslatedExternalLink } from '../common/links/translated-external-link'
import { ShowIf } from '../common/show-if/show-if'
import { getAndSetUser } from '../login-page/auth/utils'
import { SpecialUrls } from '../../api/config/types'
import { fetchAndSetUser } from '../login-page/auth/utils'
export enum RegisterError {
NONE = 'none',
@ -25,7 +24,7 @@ export enum RegisterError {
export const RegisterPage: React.FC = () => {
const { t } = useTranslation()
const allowRegister = useSelector((state: ApplicationState) => state.config.allowRegister)
const specialUrls: SpecialUrls = useSelector((state: ApplicationState) => state.config.specialUrls)
const specialUrls = useSelector((state: ApplicationState) => state.config.specialUrls)
const userExists = useSelector((state: ApplicationState) => !!state.user)
const [username, setUsername] = useState('')
@ -36,7 +35,7 @@ export const RegisterPage: React.FC = () => {
const doRegisterSubmit = useCallback((event: FormEvent) => {
doInternalRegister(username, password)
.then(() => getAndSetUser())
.then(() => fetchAndSetUser())
.catch((err: Error) => {
console.error(err)
setError(err.message === RegisterError.USERNAME_EXISTING ? err.message : RegisterError.OTHER)
@ -61,83 +60,84 @@ export const RegisterPage: React.FC = () => {
}
return <Fragment>
<div className='my-3'>
<h1 className='mb-4'><Trans i18nKey='login.register.title'/></h1>
<Row className='h-100 d-flex justify-content-center'>
<div className="my-3">
<h1 className="mb-4"><Trans i18nKey="login.register.title"/></h1>
<Row className="h-100 d-flex justify-content-center">
<Col lg={ 6 }>
<Card className='bg-dark mb-4 text-start'>
<Card className="bg-dark mb-4 text-start">
<Card.Body>
<Form onSubmit={ doRegisterSubmit }>
<Form.Group controlId='username'>
<Form.Label><Trans i18nKey='login.auth.username'/></Form.Label>
<Form.Group controlId="username">
<Form.Label><Trans i18nKey="login.auth.username"/></Form.Label>
<Form.Control
type='text'
size='sm'
type="text"
size="sm"
value={ username }
isValid={ username !== '' }
onChange={ (event) => setUsername(event.target.value) }
placeholder={ t('login.auth.username') }
className='bg-dark text-light'
autoComplete='username'
className="bg-dark text-light"
autoComplete="username"
autoFocus={ true }
required
/>
<Form.Text><Trans i18nKey='login.register.usernameInfo'/></Form.Text>
<Form.Text><Trans i18nKey="login.register.usernameInfo"/></Form.Text>
</Form.Group>
<Form.Group controlId='password'>
<Form.Label><Trans i18nKey='login.auth.password'/></Form.Label>
<Form.Group controlId="password">
<Form.Label><Trans i18nKey="login.auth.password"/></Form.Label>
<Form.Control
type='password'
size='sm'
type="password"
size="sm"
isValid={ password !== '' && password.length >= 8 }
onChange={ (event) => setPassword(event.target.value) }
placeholder={ t('login.auth.password') }
className='bg-dark text-light'
className="bg-dark text-light"
minLength={ 8 }
autoComplete='new-password'
autoComplete="new-password"
required
/>
<Form.Text><Trans i18nKey='login.register.passwordInfo'/></Form.Text>
<Form.Text><Trans i18nKey="login.register.passwordInfo"/></Form.Text>
</Form.Group>
<Form.Group controlId='re-password'>
<Form.Label><Trans i18nKey='login.register.passwordAgain'/></Form.Label>
<Form.Group controlId="re-password">
<Form.Label><Trans i18nKey="login.register.passwordAgain"/></Form.Label>
<Form.Control
type='password'
size='sm'
type="password"
size="sm"
isInvalid={ passwordAgain !== '' && password !== passwordAgain }
isValid={ passwordAgain !== '' && password === passwordAgain }
onChange={ (event) => setPasswordAgain(event.target.value) }
placeholder={ t('login.register.passwordAgain') }
className='bg-dark text-light'
autoComplete='new-password'
className="bg-dark text-light"
autoComplete="new-password"
required
/>
</Form.Group>
<ShowIf condition={ !!specialUrls?.termsOfUse || !!specialUrls?.privacy }>
<Trans i18nKey='login.register.infoTermsPrivacy'/>
<ShowIf condition={ !!specialUrls.termsOfUse || !!specialUrls.privacy }>
<Trans i18nKey="login.register.infoTermsPrivacy"/>
<ul>
<ShowIf condition={ !!specialUrls?.termsOfUse }>
<ShowIf condition={ !!specialUrls.termsOfUse }>
<li>
<TranslatedExternalLink i18nKey='landing.footer.termsOfUse' href={ specialUrls.termsOfUse }/>
<TranslatedExternalLink i18nKey="landing.footer.termsOfUse"
href={ specialUrls.termsOfUse ?? '' }/>
</li>
</ShowIf>
<ShowIf condition={ !!specialUrls?.privacy }>
<ShowIf condition={ !!specialUrls.privacy }>
<li>
<TranslatedExternalLink i18nKey='landing.footer.privacy' href={ specialUrls.privacy }/>
<TranslatedExternalLink i18nKey="landing.footer.privacy" href={ specialUrls.privacy ?? '' }/>
</li>
</ShowIf>
</ul>
</ShowIf>
<Button
variant='primary'
type='submit'
variant="primary"
type="submit"
block={ true }
disabled={ !ready }>
<Trans i18nKey='login.register.title'/>
<Trans i18nKey="login.register.title"/>
</Button>
</Form>
<br/>
<Alert show={ error !== RegisterError.NONE } variant='danger'>
<Alert show={ error !== RegisterError.NONE } variant="danger">
<Trans i18nKey={ `login.register.error.${ error }` }/>
</Alert>
</Card.Body>

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const useBackendBaseUrl = (): string => {
return process.env.REACT_APP_BACKEND_BASE_URL ?? '/mock-backend'
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useBackendBaseUrl } from './use-backend-base-url'
export const useCustomizeAssetsUrl = (): string => {
const backendBaseUrl = useBackendBaseUrl()
return (process.env.REACT_APP_CUSTOMIZE_ASSETS_URL || `${ backendBaseUrl }/public`)
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useFrontendBaseUrl } from './use-frontend-base-url'
export const useFrontendAssetsUrl = (): string => {
const frontendBaseUrl = useFrontendBaseUrl()
return (process.env.REACT_APP_FRONTEND_ASSETS_URL || frontendBaseUrl)
}

View file

@ -27,10 +27,11 @@ import { isTestMode } from './utils/test-modes'
const EditorPage = React.lazy(() => import(/* webpackPrefetch: true *//* webpackChunkName: "editor" */ './components/editor-page/editor-page'))
const RenderPage = React.lazy(() => import (/* webpackPrefetch: true *//* webpackChunkName: "renderPage" */ './components/render-page/render-page'))
const DocumentReadOnlyPage = React.lazy(() => import (/* webpackPrefetch: true *//* webpackChunkName: "documentReadOnly" */ './components/document-read-only-page/document-read-only-page'))
const baseUrl = new URL(document.head.baseURI).pathname
ReactDOM.render(
<Provider store={ store }>
<Router>
<Router basename={ baseUrl }>
<ApplicationLoader>
<ErrorBoundary>
<Switch>
@ -93,4 +94,3 @@ if (isTestMode()) {
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorkerRegistration.unregister()

View file

@ -9,5 +9,5 @@ export const isTestMode = (): boolean => {
}
export const isMockMode = (): boolean => {
return process.env.REACT_APP_BACKEND === undefined
return process.env.REACT_APP_BACKEND_BASE_URL === undefined
}