diff --git a/packages/client-core/src/admin/common/Project/ProjectFields.tsx b/packages/client-core/src/admin/common/Project/ProjectFields.tsx index 2addc06b39..6bb86a1433 100644 --- a/packages/client-core/src/admin/common/Project/ProjectFields.tsx +++ b/packages/client-core/src/admin/common/Project/ProjectFields.tsx @@ -233,8 +233,8 @@ const ProjectFields = ({ ProjectUpdateService.setSourceProjectName(project.name, '') return } - const valueRegex = new RegExp(`^${value}`, 'g') - let matchingCommit = commitData.find((data) => valueRegex.test(data.commitSHA)) + + let matchingCommit = commitData.find((data) => data.commitSHA.startsWith(value)) if (!matchingCommit) { const commitResponse = (await ProjectService.checkUnfetchedCommit({ url: projectUpdateStatus.value.sourceURL, @@ -251,7 +251,7 @@ const ProjectFields = ({ resolve(null) }, 100) }) - matchingCommit = commitData.find((data) => valueRegex.test(data.commitSHA)) + matchingCommit = commitData.find((data) => data.commitSHA.startsWith(value)) } } ProjectUpdateService.setSourceProjectName(project.name, matchingCommit?.projectName || '') diff --git a/packages/client-core/src/admin/components/locations/AddEditLocationModal.tsx b/packages/client-core/src/admin/components/locations/AddEditLocationModal.tsx index 969024a3e3..83deea0895 100644 --- a/packages/client-core/src/admin/components/locations/AddEditLocationModal.tsx +++ b/packages/client-core/src/admin/components/locations/AddEditLocationModal.tsx @@ -49,7 +49,13 @@ const locationTypeOptions = [ { label: 'Showroom', value: 'showroom' } ] -export default function AddEditLocationModal({ location }: { location?: LocationType }) { +export default function AddEditLocationModal({ + location, + sceneID +}: { + location?: LocationType + sceneID?: string | null +}) { const { t } = useTranslation() const locationMutation = useMutation(locationPath) @@ -59,7 +65,8 @@ export default function AddEditLocationModal({ location }: { location?: Location const name = useHookstate(location?.name || '') const maxUsers = useHookstate(location?.maxUsersPerInstance || 20) - const scene = useHookstate(location?.sceneId || '') + + const scene = useHookstate((location ? location.sceneId : sceneID) ?? '') const videoEnabled = useHookstate(location?.locationSetting.videoEnabled || true) const audioEnabled = useHookstate(location?.locationSetting.audioEnabled || true) const screenSharingEnabled = useHookstate(location?.locationSetting.screenSharingEnabled || true) diff --git a/packages/client-core/src/admin/components/project/AddEditProjectModal.tsx b/packages/client-core/src/admin/components/project/AddEditProjectModal.tsx index cde67234ab..f88e8da0a3 100644 --- a/packages/client-core/src/admin/components/project/AddEditProjectModal.tsx +++ b/packages/client-core/src/admin/components/project/AddEditProjectModal.tsx @@ -169,8 +169,8 @@ export default function AddEditProjectModal({ ProjectUpdateService.setSourceProjectName(project.name, '') return } - const valueRegex = new RegExp(`^${commitValue}`, 'g') - let matchingCommit = commitData.find((data) => valueRegex.test(data.commitSHA)) + + let matchingCommit = commitData.find((data) => data.commitSHA.startsWith(commitValue)) if (!matchingCommit) { const commitResponse = (await ProjectService.checkUnfetchedCommit({ url: projectUpdateStatus.value.sourceURL, @@ -187,7 +187,7 @@ export default function AddEditProjectModal({ resolve(null) }, 100) }) - matchingCommit = commitData.find((data) => valueRegex.test(data.commitSHA)) + matchingCommit = commitData.find((data) => data.commitSHA.startsWith(commitValue)) } } ProjectUpdateService.setSourceProjectName(project.name, matchingCommit?.projectName || '') diff --git a/packages/client-core/src/social/services/InviteService.ts b/packages/client-core/src/social/services/InviteService.ts index 3ba40d81b0..b71085fcf8 100644 --- a/packages/client-core/src/social/services/InviteService.ts +++ b/packages/client-core/src/social/services/InviteService.ts @@ -26,12 +26,7 @@ Ethereal Engine. All Rights Reserved. import { Paginated } from '@feathersjs/feathers' import { useEffect } from 'react' -import { - EMAIL_REGEX, - INVITE_CODE_REGEX, - PHONE_REGEX, - USER_ID_REGEX -} from '@etherealengine/common/src/constants/IdConstants' +import { EMAIL_REGEX, INVITE_CODE_REGEX, PHONE_REGEX, USER_ID_REGEX } from '@etherealengine/common/src/regex' import { InviteCode, InviteData, diff --git a/packages/client-core/src/user/components/UserMenu/menus/ShareMenu.tsx b/packages/client-core/src/user/components/UserMenu/menus/ShareMenu.tsx index a3cc5cbd58..0a816ec9a2 100755 --- a/packages/client-core/src/user/components/UserMenu/menus/ShareMenu.tsx +++ b/packages/client-core/src/user/components/UserMenu/menus/ShareMenu.tsx @@ -33,8 +33,8 @@ import InputCheck from '@etherealengine/client-core/src/common/components/InputC import InputText from '@etherealengine/client-core/src/common/components/InputText' import Menu from '@etherealengine/client-core/src/common/components/Menu' import { NotificationService } from '@etherealengine/client-core/src/common/services/NotificationService' -import { EMAIL_REGEX, PHONE_REGEX } from '@etherealengine/common/src/constants/IdConstants' import multiLogger from '@etherealengine/common/src/logger' +import { EMAIL_REGEX, PHONE_REGEX } from '@etherealengine/common/src/regex' import { InviteCode, InviteData } from '@etherealengine/common/src/schema.type.module' import { useMutableState } from '@etherealengine/hyperflux' import { isShareAvailable } from '@etherealengine/spatial/src/common/functions/DetectFeatures' diff --git a/packages/client/pwa.config.ts b/packages/client/pwa.config.ts index 43843bb413..834303a1d3 100644 --- a/packages/client/pwa.config.ts +++ b/packages/client/pwa.config.ts @@ -27,6 +27,9 @@ import { VitePWA } from 'vite-plugin-pwa' import manifest from './manifest.default.json' +const WILDCARD_REGEX = /^\/.*$/ +const LOCAL_FILESYSTEM_REGEX = /^\/@fs\/.*$/ + /** * Creates a new instance of the VitePWA plugin for Vite.js. * @param {Object} clientSetting - An object containing custom settings for the PWA. @@ -75,9 +78,9 @@ const PWA = (clientSetting) => // Allowlist all paths for navigateFallback during development navigateFallbackAllowlist: [ // allow everything - new RegExp('^/.*$'), + WILDCARD_REGEX, // allow @fs - new RegExp('^/@fs/.*$') + LOCAL_FILESYSTEM_REGEX ] }, workbox: { @@ -94,7 +97,7 @@ const PWA = (clientSetting) => // Allowlist all paths for navigateFallback during production navigateFallbackAllowlist: [ // allow everything - new RegExp('^/.*$') + WILDCARD_REGEX ], // Set the glob directory and patterns for the cache globDirectory: process.env.APP_ENV === 'development' ? './public' : './dist', diff --git a/packages/common/src/config.ts b/packages/common/src/config.ts index f569349298..ea8726b97e 100644 --- a/packages/common/src/config.ts +++ b/packages/common/src/config.ts @@ -23,16 +23,14 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ +import { EMAIL_REGEX } from './regex' + /** * Config settings (for client and isomorphic engine usage). */ const localBuildOrDev = globalThis.process.env.APP_ENV === 'development' || globalThis.process.env.VITE_LOCAL_BUILD === 'true' -// https://fightingforalostcause.net/content/misc/2006/compare-email-regex.php -const EMAIL_REGEX = - /^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i - export function validateEmail(email: string): boolean { return EMAIL_REGEX.test(email) } diff --git a/packages/common/src/constants/AvatarConstants.ts b/packages/common/src/constants/AvatarConstants.ts index fc5b340c26..490bf6161e 100755 --- a/packages/common/src/constants/AvatarConstants.ts +++ b/packages/common/src/constants/AvatarConstants.ts @@ -33,5 +33,3 @@ export const THUMBNAIL_FILE_ALLOWED_EXTENSIONS = '.png,.jpg' export const THUMBNAIL_WIDTH = 300 export const THUMBNAIL_HEIGHT = 300 export const MAX_ALLOWED_TRIANGLES = 100000 -export const REGEX_VALID_URL = - /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g diff --git a/packages/common/src/constants/GitHubConstants.ts b/packages/common/src/constants/GitHubConstants.ts index 7cc84076b0..3d8eaa44ab 100644 --- a/packages/common/src/constants/GitHubConstants.ts +++ b/packages/common/src/constants/GitHubConstants.ts @@ -23,7 +23,4 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -export const GITHUB_URL_REGEX = /(?:git@|https:\/\/)([a-zA-Z0-9\-]+:[a-zA-Z0-9_]+@)?github.com[:/](.*)[.git]?/ export const GITHUB_PER_PAGE = 100 -export const PUBLIC_SIGNED_REGEX = /https:\/\/[\w\d\s\-_]+:[\w\d\s\-_]+@github.com\/([\w\d\s\-_]+)\/([\w\d\s\-_]+).git/ -export const INSTALLATION_SIGNED_REGEX = /https:\/\/oauth2:[\w\d\s\-_]+@github.com\/([\w\d\s\-_]+)\/([\w\d\s\-_]+).git/ diff --git a/packages/common/src/constants/IdConstants.ts b/packages/common/src/constants/IdConstants.ts deleted file mode 100644 index 0ebaa7a6c0..0000000000 --- a/packages/common/src/constants/IdConstants.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* -CPAL-1.0 License - -The contents of this file are subject to the Common Public Attribution License -Version 1.0. (the "License"); you may not use this file except in compliance -with the License. You may obtain a copy of the License at -https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. -The License is based on the Mozilla Public License Version 1.1, but Sections 14 -and 15 have been added to cover use of software over a computer network and -provide for limited attribution for the Original Developer. In addition, -Exhibit A has been modified to be consistent with Exhibit B. - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -specific language governing rights and limitations under the License. - -The Original Code is Ethereal Engine. - -The Original Developer is the Initial Developer. The Initial Developer of the -Original Code is the Ethereal Engine team. - -All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 -Ethereal Engine. All Rights Reserved. -*/ - -export const USER_ID_REGEX = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/ -export const EMAIL_REGEX = - /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/ -export const PHONE_REGEX = /^[0-9]{10}$/ -export const INVITE_CODE_REGEX = /^[0-9a-fA-F]{8}$/ diff --git a/packages/common/src/constants/ProjectKeyConstants.ts b/packages/common/src/constants/ProjectKeyConstants.ts deleted file mode 100644 index 3dc72cc700..0000000000 --- a/packages/common/src/constants/ProjectKeyConstants.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* -CPAL-1.0 License - -The contents of this file are subject to the Common Public Attribution License -Version 1.0. (the "License"); you may not use this file except in compliance -with the License. You may obtain a copy of the License at -https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. -The License is based on the Mozilla Public License Version 1.1, but Sections 14 -and 15 have been added to cover use of software over a computer network and -provide for limited attribution for the Original Developer. In addition, -Exhibit A has been modified to be consistent with Exhibit B. - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -specific language governing rights and limitations under the License. - -The Original Code is Ethereal Engine. - -The Original Developer is the Initial Developer. The Initial Developer of the -Original Code is the Ethereal Engine team. - -All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 -Ethereal Engine. All Rights Reserved. -*/ - -//FIXME: Legacy location of public assets, should be deleted and all files' use of this removed once all existing -//projects have had their public assets moved to the /public folder -export const assetsRegex = /projects\/[a-zA-Z0-9-_\/]+\/assets\// -export const projectRegex = /projects\/[a-zA-Z0-9-_\/]+/ -export const projectPublicRegex = /projects\/[a-zA-Z0-9-_\/]+\/public\// -export const projectThumbnailsRegex = /projects\/[a-zA-Z0-9-_\/]+\/thumbnails\// diff --git a/packages/common/src/regex/index.ts b/packages/common/src/regex/index.ts new file mode 100644 index 0000000000..957b4f3140 --- /dev/null +++ b/packages/common/src/regex/index.ts @@ -0,0 +1,135 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +// eslint-disable-next-line no-control-regex +export const INVALID_FILENAME_REGEX = /[_<>:"/\\|?*\u0000-\u001F]/g +export const WINDOWS_RESERVED_NAME_REGEX = /^(con|prn|aux|nul|com\d|lpt\d)$/i +export const VALID_SCENE_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_\-\s]{1,62}[a-zA-Z0-9_\-]$/ + +/** + * Matches CSS imports & URLS. + * For eg: `@import "styles.css"`, `url(image.png)`. Captures the resource in group 2 or group 3. + */ +export const CSS_URL_REGEX = /(@import\s+["']([^"']+)["']|url\((?!['"]?(?:data):)['"]?([^'"\)]+)['"]?\))/gi + +/** + * Matches absolute URLs. For eg: `http://example.com`, `https://example.com`, `ftp://example.com`, `//example.com`, etc. + * This Does NOT match relative URLs like `example.com` + */ +export const ABSOLUTE_URL_PROTOCOL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/ + +/** + * Captures project name and asset path from a URL. + * For eg: `/path/to/projects/project123/assets/images/logo.png` will capture following groups + * - `project123` => Group 1 + * - `assets/images/logo.png` => Group 2 + */ +export const STATIC_ASSET_REGEX = /^(?:.*\/(?:projects|static-resources)\/([^\/]*)\/((?:assets\/|).*)$)/ + +// ===================================================================== +// ========================= ID Regex Patterns ========================= +// ===================================================================== + +/** + * This regex is used to validate a string that conforms to the UUID version 4 format. It ensures that the string consists of exactly 32 hexadecimal digits arranged in the 8-4-4-4-12 pattern, separated by hyphens. + */ +export const USER_ID_REGEX = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/ + +/** + * Email regex. Source: https://fightingforalostcause.net/content/misc/2006/compare-email-regex.php + */ +export const EMAIL_REGEX = + /^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i + +/** + * This regex is useful for validating input that must be exactly 10 digits long, such as a phone number (without any separators or formatting). + */ +export const PHONE_REGEX = /^[0-9]{10}$/ + +/** + * This regex is used to validate strings that should consist of exactly 8 hexadecimal digits. + */ +export const INVITE_CODE_REGEX = /^[0-9a-fA-F]{8}$/ + +// ========================================================================= +// ========================= GitHub Regex Patterns ========================= +// ========================================================================= + +/** + * This regular expression is designed to match GitHub repository URLs in both SSH and HTTPS formats. Group 2 captures the owner and repository name. + * - For eg: `git@github.com:user/repo.git` + * - `user/repo` => Group 1 + */ +export const GITHUB_URL_REGEX = /(?:git@github.com:|https:\/\/github.com\/)([a-zA-Z0-9_\-]+\/[a-zA-Z0-9_\-]+)(\.git)?$/ + +/** + * This regex is useful for extracting the owner and repository name from GitHub HTTPS URLs that contain authentication credentials in the URL itself. Group 1 captures the owner and Group 2 captures the repository name. + * - For eg: `https://username:password@github.com/owner/repository-name.git` + * - `owner` => Group 1 + * - `repository-name` => Group 2 + */ +export const PUBLIC_SIGNED_REGEX = /https:\/\/[\w\d\s\-_]+:[\w\d\s\-_]+@github.com\/([\w\d\s\-_]+)\/([\w\d\s\-_]+).git$/ + +/** + * This regex is useful for extracting the owner and repository name from GitHub HTTPS URLs that contain OAuth2 token or credentials in the URL itself. Group 1 captures the owner and Group 2 captures the repository name. + * - For eg: `https://oauth2:token123@github.com/owner/repository-name.git` + * - `owner` => Group 1 + * - `repository-name` => Group 2 + */ +export const INSTALLATION_SIGNED_REGEX = /https:\/\/oauth2:[\w\d\s\-_]+@github.com\/([\w\d\s\-_]+)\/([\w\d\s\-_]+).git$/ + +// ============================================================================== +// ========================= Project Key Regex Patterns ========================= +// ============================================================================== + +/** + * This regex is used to match specific file paths or directory structures that start with `projects/`, followed by one or more valid characters (letters, digits, hyphens, underscores, or forward slashes), and end with `/assets/` + */ +export const ASSETS_REGEX = /projects\/[a-zA-Z0-9-_\/]+\/assets\// + +/** + * This regex matches strings that start with `projects/`, followed by one or more characters that can be letters, digits, hyphens, underscores, or forward slashes. + */ +export const PROJECT_REGEX = /projects\/[a-zA-Z0-9-_\/]+/ + +/** + * This regex matches strings that start with `projects/`, followed by one or more characters that can be letters, digits, hyphens, underscores, or forward slashes, and then `/public/`. + */ +export const PROJECT_PUBLIC_REGEX = /projects\/[a-zA-Z0-9-_\/]+\/public\// + +/** + * This regex matches strings that start with `projects/`, followed by one or more characters that can be letters, digits, hyphens, underscores, or forward slashes, and then `/thumbnails/`. + */ +export const PROJECT_THUMBNAIL_REGEX = /projects\/[a-zA-Z0-9-_\/]+\/thumbnails\// + +export const VALID_PROJECT_NAME = /^(?!\s)[\w\-\s]+$/ + +// ========================================================================== +// ========================= Dynamic Regex Patterns ========================= +// ========================================================================== + +export const getCacheRegex = (fileServer: string) => { + return new RegExp(`${fileServer}\/projects`) +} diff --git a/packages/common/src/utils/validateSceneName.ts b/packages/common/src/utils/validateSceneName.ts index 721034d0ab..90b283f97c 100644 --- a/packages/common/src/utils/validateSceneName.ts +++ b/packages/common/src/utils/validateSceneName.ts @@ -23,17 +23,18 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -// eslint-disable-next-line no-control-regex -const invalidFileNameRegex = /[_<>:"/\\|?*\u0000-\u001F]/g -const windowsReservedNameRegex = /^(con|prn|aux|nul|com\d|lpt\d)$/i -const validSceneNameRegex = /^[a-zA-Z0-9][a-zA-Z0-9_\-\s]{1,62}[a-zA-Z0-9_\-]$/ +import { + INVALID_FILENAME_REGEX, + VALID_SCENE_NAME_REGEX, + WINDOWS_RESERVED_NAME_REGEX +} from '@etherealengine/common/src/regex' export default function isValidSceneName(sceneName: string) { return ( sceneName.length >= 3 && sceneName.length <= 64 && - !invalidFileNameRegex.test(sceneName) && - !windowsReservedNameRegex.test(sceneName) && - validSceneNameRegex.test(sceneName) + !INVALID_FILENAME_REGEX.test(sceneName) && + !WINDOWS_RESERVED_NAME_REGEX.test(sceneName) && + VALID_SCENE_NAME_REGEX.test(sceneName) ) } diff --git a/packages/common/tests/regex.test.ts b/packages/common/tests/regex.test.ts new file mode 100644 index 0000000000..d8a503b597 --- /dev/null +++ b/packages/common/tests/regex.test.ts @@ -0,0 +1,672 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ +import assert from 'assert' +import { + ABSOLUTE_URL_PROTOCOL_REGEX, + ASSETS_REGEX, + CSS_URL_REGEX, + EMAIL_REGEX, + GITHUB_URL_REGEX, + INSTALLATION_SIGNED_REGEX, + INVALID_FILENAME_REGEX, + INVITE_CODE_REGEX, + PHONE_REGEX, + PROJECT_PUBLIC_REGEX, + PROJECT_REGEX, + PROJECT_THUMBNAIL_REGEX, + PUBLIC_SIGNED_REGEX, + STATIC_ASSET_REGEX, + USER_ID_REGEX, + VALID_PROJECT_NAME, + VALID_SCENE_NAME_REGEX, + WINDOWS_RESERVED_NAME_REGEX +} from '../src/regex' + +describe('regex.test', () => { + describe('INVALID_FILENAME_REGEX', () => { + it('should match invalid filenames', () => { + const invalidFilenames = [ + 'hello_world', + 'file { + assert.ok(INVALID_FILENAME_REGEX.test(filename), `Expected '${filename}' to be invalid`) + INVALID_FILENAME_REGEX.lastIndex = 0 + }) + }) + + it('should not match valid filenames', () => { + const validFilenames = [ + 'helloworld', + 'filename', + 'emailexample.com', + 'pathtofile', + 'backslash', + 'pipesymbol', + 'questionmark', + 'asteriskchar', + 'controlchar', + 'anothercontrol' + ] + validFilenames.forEach((filename) => { + assert.ok(!INVALID_FILENAME_REGEX.test(filename), `Expected '${filename}' to be valid`) + INVALID_FILENAME_REGEX.lastIndex = 0 + }) + }) + }) + + describe('WINDOWS_RESERVED_NAME_REGEX', () => { + it('should match windows reserved names', () => { + const reservedNames = ['CON', 'PrN', 'auX', 'NUL', 'COM0', 'com1', 'Com9', 'LPT0', 'LpT4', 'lPt9'] + reservedNames.forEach((filename) => { + assert.ok(WINDOWS_RESERVED_NAME_REGEX.test(filename), `Expected '${filename}' to be windows reserved name`) + }) + }) + + it('should not match common filenames', () => { + const validFilenames = [ + 'helloworld', + 'filename', + 'emailexample.com', + 'pathtofile', + 'backslash', + 'pipesymbol', + 'questionmark', + 'asteriskchar', + 'controlchar', + 'anothercontrol' + ] + validFilenames.forEach((filename) => { + assert.ok(!WINDOWS_RESERVED_NAME_REGEX.test(filename), `Expected '${filename}' to be valid filename`) + }) + }) + }) + + describe('VALID_SCENE_NAME_REGEX', () => { + it('should match valid scene names', () => { + const validSceneNames = [ + 'A123', + 'file_name', + 'user-name', + '12345', + 'test file', + 'my-file_123', + 'Name123_456-789', + 'a_b_c_d-e_f_g-h_i_j-k_l_m-n_o-p-q_r-s_t-u_v-w_x-y_z0123456789_-' + ] + validSceneNames.forEach((filename) => { + assert.ok(VALID_SCENE_NAME_REGEX.test(filename), `Expected '${filename}' to be valid scene names`) + }) + }) + + it('should not match invalid scene names', () => { + const invalidSceneNames = [ + 'A1', + '_test', + 'invalid!', + 'very_long_string_that_is_definitely_not_going_to_match_the_regex_because_it_is_way_too_long_for_the_pattern', + '--double-hyphen', + '...' + ] + invalidSceneNames.forEach((filename) => { + assert.ok(!VALID_SCENE_NAME_REGEX.test(filename), `Expected '${filename}' to be invalid scene names`) + }) + }) + }) + + describe('CSS_URL_REGEX', () => { + it('should match for CSS sources with valid URLs', () => { + const positiveCases = [ + { + source: '@import url("https://example.com/styles.css");', + urlResource: 'https://example.com/styles.css' + }, + { + source: 'background: url("https://example.com/image.jpg");', + urlResource: 'https://example.com/image.jpg' + }, + { + source: "background: url('https://example.com/image.jpg');", + urlResource: 'https://example.com/image.jpg' + }, + { + source: 'background: url(https://example.com/image.jpg);', + urlResource: 'https://example.com/image.jpg' + } + ] + + positiveCases.forEach(({ source, urlResource }) => { + const match = CSS_URL_REGEX.exec(source) + assert.ok(match, `Expected '${source}' to match CSS_URL_REGEX`) + assert.equal(match?.[2] ?? match?.[3], urlResource, `Expected URL resource: ${urlResource} in '${source}'`) + CSS_URL_REGEX.lastIndex = 0 // reset the regex (important for patterns with global flag) + }) + }) + + it('should not match invalid CSS imports & URLs', () => { + const negativeCases = [ + 'color: #fff;', + 'background: urll(https://example.com/image.jpg);', // Misspelled 'url' + 'url(https://example.com/image', // Missing closing parenthesis + 'background: url();' // Empty URL + ] + + negativeCases.forEach((source) => { + assert.ok(!CSS_URL_REGEX.test(source), `Expected '${source}' to not match CSS_URL_REGEX`) + }) + }) + }) + + describe('ABSOLUTE_URL_REGEX', () => { + it('should match absolute URLs', () => { + const positiveCases = ['http://example.com', 'https://example.com', 'ftp://example.com', '//example.com'] + + positiveCases.forEach((url) => { + assert.match(url, ABSOLUTE_URL_PROTOCOL_REGEX, `Expected '${url}' to match ABSOLUTE_URL_REGEX`) + }) + }) + + it('should not match relative URLs', () => { + assert.doesNotMatch( + 'example.com', + ABSOLUTE_URL_PROTOCOL_REGEX, + `Expected 'example.com' to not match ABSOLUTE_URL_REGEX` + ) + }) + }) + + describe('STATIC_ASSET_REGEX', () => { + it('should match static asset URLs', () => { + const positiveCases = [ + { + url: 'https://example.com/projects/default-project/assets/images/logo.png', + projectName: 'default-project', + assetPath: 'assets/images/logo.png' + }, + { + url: 'https://example.com/static-resources/default-project/assets/images/logo.png', + projectName: 'default-project', + assetPath: 'assets/images/logo.png' + }, + { + url: 'https://example.com/projects/default-project/assets/animations/emotes.glb', + projectName: 'default-project', + assetPath: 'assets/animations/emotes.glb' + }, + { + url: 'https://example.com/projects/default-project/assets/animations/locomotion.glb', + projectName: 'default-project', + assetPath: 'assets/animations/locomotion.glb' + } + ] + positiveCases.forEach(({ url, projectName, assetPath }) => { + const match = STATIC_ASSET_REGEX.exec(url) + assert.ok(match, `Expected '${url}' to match STATIC_ASSET_REGEX`) + assert.equal(match?.[1], projectName, `Expected project name '${projectName}' in '${url}'. Found ${match?.[1]}`) + assert.equal(match?.[2], assetPath, `Expected asset path '${assetPath}' in '${url}'. Found ${match?.[2]}`) + }) + }) + + it('should not match non-static asset URLs', () => { + const negativeCases = [ + 'https://example.com/static-resources/', + 'https://example.com/project/subdir/assets', + 'https://example.com/default-project/assets/animations/emotes.glb' + ] + negativeCases.forEach((url) => { + assert.doesNotMatch(url, STATIC_ASSET_REGEX, `Expected '${url}' to not match STATIC_ASSET_REGEX`) + }) + }) + }) + + describe('USER_ID_REGEX', () => { + it('should match valid user Ids', () => { + const positiveCases = [ + '123e4567-e89b-12d3-a456-426614174000', + 'abcdef01-2345-6789-abcd-ef0123456789', + 'ABCDEF01-2345-6789-ABCD-EF0123456789', + 'a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d', + 'ffffffff-ffff-ffff-ffff-ffffffffffff' + ] + positiveCases.forEach((userId) => { + assert.match(userId, USER_ID_REGEX, `Expected '${userId}' to match USER_ID_REGEX`) + }) + }) + + it('should not match invalid user Ids', () => { + const negativeCases = [ + '123e4567-e89b-12d3-a456-42661417400', + '123e4567e89b12d3a456426614174000', + '123e4567-e89b-12d3-a456-426614174000-', + '-123e4567-e89b-12d3-a456-426614174000', + '123e4567-e89b-12d3-a456-4266141740000', + '123e4567-e89b-12d3-g456-426614174000' + ] + negativeCases.forEach((userId) => { + assert.doesNotMatch(userId, USER_ID_REGEX, `Expected '${userId}' to not match USER_ID_REGEX`) + }) + }) + }) + + describe('EMAIL_REGEX', () => { + it('should match valid emails', () => { + const positiveCases = [ + 'simple@example.com', + 'very.common@example.com', + 'disposable.style.email.with+symbol@example.com', + 'other.email-with-hyphen@example.com', + 'fully-qualified-domain@example.com', + 'user.name+tag+sorting@example.com', + 'x@example.com', + 'example-indeed@strange-example.com', + 'mailhost!username@example.org', + 'user%example.com@example.org', + 'user-@example.org' + ] + + positiveCases.forEach((email) => { + assert.match(email, EMAIL_REGEX, `Expected '${email}' to match EMAIL_REGEX`) + }) + }) + + it('should not match invalid emails', () => { + const negativeCases = [ + 'plainaddress', + '@missingusername.com', + 'username@.com.', + '.username@yahoo.com', + 'username@yahoo.com.', + 'username@yahoo..com', + 'username@yahoo.c', + 'username@yahoo.corporate', + 'username@-example.com', + 'username@example.com-', + 'username@example..com', + 'username@111.222.333.44444', + 'username@example..com', + 'username@-example.com' + ] + negativeCases.forEach((email) => { + assert.doesNotMatch(email, EMAIL_REGEX, `Expected '${email}' to not match EMAIL_REGEX`) + }) + }) + }) + + describe('PHONE_REGEX', () => { + it('should match valid phone numbers', () => { + const positiveCases = ['1234567890', '0987654321', '0000000000', '1111111111', '9999999999'] + + positiveCases.forEach((phoneNo) => { + assert.match(phoneNo, PHONE_REGEX, `Expected '${phoneNo}' to match PHONE_REGEX`) + }) + }) + + it('should not match invalid phone numbers', () => { + const negativeCases = [ + '123456789', + '12345678901', + '12345abcde', + '123 456 7890', + '123-456-7890', + '+1234567890', + '0123456789a', + 'a123456789', + '12345 67890', + `` + ] + negativeCases.forEach((phoneNo) => { + assert.doesNotMatch(phoneNo, PHONE_REGEX, `Expected '${phoneNo}' to not match PHONE_REGEX`) + }) + }) + }) + + describe('INVITE_CODE_REGEX', () => { + it('should match valid invite codes', () => { + const positiveCases = ['1a2b3c4d', 'A1B2C3D4', 'abcdef12', '12345678', 'ABCDEF12'] + + positiveCases.forEach((inviteCode) => { + assert.match(inviteCode, INVITE_CODE_REGEX, `Expected '${inviteCode}' to match INVITE_CODE_REGEX`) + }) + }) + + it('should not match invalid invite codes', () => { + const negativeCases = [ + '1a2b3c4', + '1a2b3c4d5', + '1a2b3c4g', + '1a2b 3c4d', + '1a-2b-3c-4d', + '1234abcd!', + 'GHIJKLMN', + '123' + ] + negativeCases.forEach((inviteCode) => { + assert.doesNotMatch(inviteCode, INVITE_CODE_REGEX, `Expected '${inviteCode}' to not match INVITE_CODE_REGEX`) + }) + }) + }) + + describe('GITHUB_URL_REGEX', () => { + it('should match valid GitHub Repository URLs', () => { + const positiveCases = [ + { + url: 'git@github.com:user/repo.git', + capturedGroup: 'user/repo' + }, + { + url: 'https://github.com/user/repo.git', + capturedGroup: 'user/repo' + }, + { + url: 'git@github.com:user/repo', + capturedGroup: 'user/repo' + }, + { + url: 'https://github.com/user/repo', + capturedGroup: 'user/repo' + }, + { + url: 'git@github.com:username_123/repo-name.git', + capturedGroup: 'username_123/repo-name' + } + ] + + positiveCases.forEach(({ url, capturedGroup }) => { + const match = GITHUB_URL_REGEX.exec(url) + assert.ok(match, `Expected '${url}' to match GITHUB_URL_REGEX`) + assert.equal(match?.[1], capturedGroup, `Expected group '${capturedGroup}' in '${url}'. Found ${match?.[1]}`) + }) + }) + + it('should not match invalid GitHub Repository URLs', () => { + const negativeCases = [ + 'git@bitbucket.org:user/repo.git', // different domain + 'http://github.com/user/repo.git', // uses HTTP instead of HTTPS + 'git@github.comuser/repo.git', // missing colon + 'https://bitbucket.org/user/repo', // different domain + 'git@github.com:/user/repo.git', // extra colon + 'https://github.com:user/repo.git', // colon in HTTPS format + 'git://github.com/user/repo.git', // unsupported protocol + 'git@github.com:username/repo.', // ends with a dot instead of .git + 'https://github.com/username/repo.git/extra', // extra path segment + 'git@github.com:username' // missing repository name + ] + negativeCases.forEach((url) => { + assert.doesNotMatch(url, GITHUB_URL_REGEX, `Expected '${url}' to not match GITHUB_URL_REGEX`) + }) + }) + }) + + describe('PUBLIC_SIGNED_REGEX', () => { + it('should match valid public signed GitHub Repository URLs', () => { + const positiveCases = [ + { + url: 'https://username:password@github.com/owner/repo.git', + owner: 'owner', + repo: 'repo' + }, + { + url: 'https://user-name:pass-word@github.com/owner-name/repo-name.git', + owner: 'owner-name', + repo: 'repo-name' + }, + { + url: 'https://user_name:pass_word@github.com/owner_name/repo_name.git', + owner: 'owner_name', + repo: 'repo_name' + }, + { + url: 'https://user name:pass word@github.com/owner name/repo name.git', + owner: 'owner name', + repo: 'repo name' + }, + { + url: 'https://user123:pass123@github.com/owner123/repo123.git', + owner: 'owner123', + repo: 'repo123' + }, + { + url: 'https://user_name-123:pass_word-123@github.com/owner_name-123/repo_name-123.git', + owner: 'owner_name-123', + repo: 'repo_name-123' + } + ] + + positiveCases.forEach(({ url, owner, repo }) => { + const match = PUBLIC_SIGNED_REGEX.exec(url) + assert.ok(match, `Expected '${url}' to match PUBLIC_SIGNED_REGEX`) + assert.equal(match?.[1], owner, `Expected owner '${owner}' in '${url}'. Found ${match?.[1]}`) + assert.equal(match?.[2], repo, `Expected repo '${repo}' in '${url}'. Found ${match?.[2]}`) + }) + }) + + it('should not match invalid public signed GitHub Repository URLs', () => { + const negativeCases = [ + 'http://username:password@github.com/owner/repo.git', // uses HTTP instead of HTTPS + 'https://username:password@bitbucket.com/owner/repo.git', // different domain + 'https://username:password@github.com/owner/repo', // missing .git suffix + 'https://username:password@github.com/owner/repo/extra.git', // extra path segment + 'https://username:password@github.com//repo.git', // missing owner name + 'https://username:password@github.com/owner/.git', // missing repository name + 'https://username:password@github.com/owner', // missing repository name and .git + 'https://username@github.com/owner/repo.git', // missing password + 'https://:password@github.com/owner/repo.git', // missing username + 'https://username:password@github.com/owner/repo.git/extra' // extra path after .git + ] + negativeCases.forEach((url) => { + assert.doesNotMatch(url, PUBLIC_SIGNED_REGEX, `Expected '${url}' to not match PUBLIC_SIGNED_REGEX`) + }) + }) + }) + + describe('INSTALLATION_SIGNED_REGEX', () => { + it('should match valid Installation signed GitHub Repository URLs', () => { + const positiveCases = [ + { + url: 'https://oauth2:token123@github.com/owner/repo.git', + owner: 'owner', + repo: 'repo' + }, + { + url: 'https://oauth2:my-oauth-token@github.com/user-name/repository-name.git', + owner: 'user-name', + repo: 'repository-name' + }, + { + url: 'https://oauth2:abc_123@github.com/org_name/repo_name.git', + owner: 'org_name', + repo: 'repo_name' + } + ] + + positiveCases.forEach(({ url, owner, repo }) => { + const match = INSTALLATION_SIGNED_REGEX.exec(url) + assert.ok(match, `Expected '${url}' to match INSTALLATION_SIGNED_REGEX`) + assert.equal(match?.[1], owner, `Expected owner '${owner}' in '${url}'. Found ${match?.[1]}`) + assert.equal(match?.[2], repo, `Expected repo '${repo}' in '${url}'. Found ${match?.[2]}`) + }) + }) + + it('should not match invalid Installation signed GitHub Repository URLs', () => { + const negativeCases = [ + 'http://oauth2:token123@github.com/owner/repo.git', // uses HTTP instead of HTTPS + 'https://oauth2:token123@bitbucket.org/owner/repo.git', // different domain + 'https://oauth2:token123@github.com/owner/repo', // missing .git suffix + 'https://oauth2:token123@github.com/owner/repo/extra.git', // extra path segment + 'https://oauth2:token123@github.com//repo.git', // missing owner name + 'https://oauth2:token123@github.com/owner/.git', // missing repository name + 'https://oauth2:token123@github.com/owner', // missing repository name and .git + 'https://oauth2:token123@github.com/owner/repo/.git', // trailing slash after .git + 'https://oauth2:token123@github.com/owner/repo.git/extra' // extra path segment after .git + ] + negativeCases.forEach((url) => { + assert.doesNotMatch(url, INSTALLATION_SIGNED_REGEX, `Expected '${url}' to not match INSTALLATION_SIGNED_REGEX`) + }) + }) + }) + + describe('ASSETS_REGEX', () => { + it('should match assets URLs', () => { + const positiveCases = [ + 'https://example.com/projects/default-project/assets/images/logo.png', + 'https://example.com/projects/default-project/assets/animations/emotes.glb', + 'https://example.com/projects/default-project/assets/animations/locomotion.glb' + ] + positiveCases.forEach((url) => { + assert.match(url, ASSETS_REGEX, `Expected '${url}' to match ASSETS_REGEX`) + }) + }) + + it('should not match non-assets URLs', () => { + const negativeCases = [ + 'https://example.com/projects/default-project/scene.json', + 'https://example.com/projects/default-project/assets', + 'https://example.com/default-project/assets/animations/emotes.glb' + ] + negativeCases.forEach((url) => { + assert.doesNotMatch(url, ASSETS_REGEX, `Expected '${url}' to not match ASSETS_REGEX`) + }) + }) + }) + + describe('PROJECT_REGEX', () => { + it('should match valid project paths', () => { + const positiveCases = [ + 'projects/project123', + 'projects/project-name', + 'projects/project_name', + 'projects/project/123', + 'projects/project/abc_def' + ] + positiveCases.forEach((value) => { + assert.match(value, PROJECT_REGEX, `Expected '${value}' to match PROJECT_REGEX`) + }) + }) + + it('should not match invalid project paths', () => { + const negativeCases = [ + 'projects/', // (missing project name) + 'projects' // (missing trailing slash and project name) + ] + negativeCases.forEach((value) => { + assert.doesNotMatch(value, PROJECT_REGEX, `Expected '${value}' to not match PROJECT_REGEX`) + }) + }) + }) + + describe('PROJECT_PUBLIC_REGEX', () => { + it('should match valid project paths', () => { + const positiveCases = [ + 'projects/project123/public/', + 'projects/project-name/public/', + 'projects/project_name/public/', + 'projects/project/123/public/', + 'projects/project/abc_def/public/' + ] + positiveCases.forEach((value) => { + assert.match(value, PROJECT_PUBLIC_REGEX, `Expected '${value}' to match PROJECT_PUBLIC_REGEX`) + }) + }) + + it('should not match invalid project paths', () => { + const negativeCases = [ + 'projects/project123/public', // (missing trailing slash) + 'projects/project-name/private/', // (incorrect folder private instead of public) + 'projects/project$name/public/', // (contains invalid character $) + 'projects/project-@name/public/', // (contains invalid character @) + 'projects/' // (missing project name and /public/) + ] + negativeCases.forEach((value) => { + assert.doesNotMatch(value, PROJECT_PUBLIC_REGEX, `Expected '${value}' to not match PROJECT_PUBLIC_REGEX`) + }) + }) + }) + + describe('PROJECT_THUMBNAIL_REGEX', () => { + it('should match valid project thumbnail paths', () => { + const positiveCases = [ + 'projects/project123/thumbnails/', + 'projects/project-name/thumbnails/', + 'projects/project_name/thumbnails/', + 'projects/project/123/thumbnails/', + 'projects/project/abc_def/thumbnails/' + ] + positiveCases.forEach((value) => { + assert.match(value, PROJECT_THUMBNAIL_REGEX, `Expected '${value}' to match PROJECT_THUMBNAIL_REGEX`) + }) + }) + + it('should not match invalid project thumbnail paths', () => { + const negativeCases = [ + 'projects/project123/thumbnails', // (missing trailing slash) + 'projects/project-name/private/', // (incorrect folder private instead of public) + 'projects/project$name/thumbnails/', // (contains invalid character $) + 'projects/project-@name/thumbnails/', // (contains invalid character @) + 'projects/' // (missing project name and /thumbnail/) + ] + negativeCases.forEach((value) => { + assert.doesNotMatch(value, PROJECT_THUMBNAIL_REGEX, `Expected '${value}' to not match PROJECT_THUMBNAIL_REGEX`) + }) + }) + }) + + describe('VALID_PROJECT_NAME', () => { + it('should match valid project names', () => { + const positiveCases = [ + 'example', + 'example-example', + 'example example', + 'example123', + 'example-example123', + 'example example 123', + 'example-example_123', + 'example example-123', + 'example-example_123', + 'example example_123' + ] + positiveCases.forEach((name) => { + assert.match(name, VALID_PROJECT_NAME, `Expected '${name}' to match VALID_PROJECT_NAME`) + }) + }) + + it('should not match invalid project names', () => { + const negativeCases = [ + ' word', // (leading space) + 'word@word' // (contains non-word character @) + ] + negativeCases.forEach((name) => { + assert.doesNotMatch(name, VALID_PROJECT_NAME, `Expected '${name}' to not match VALID_PROJECT_NAME`) + }) + }) + }) +}) diff --git a/packages/editor/src/components/element/ElementList.tsx b/packages/editor/src/components/element/ElementList.tsx index c017617733..d5ca7173e5 100644 --- a/packages/editor/src/components/element/ElementList.tsx +++ b/packages/editor/src/components/element/ElementList.tsx @@ -126,11 +126,9 @@ const useComponentShelfCategories = (search: string) => { return Object.entries(getState(ComponentShelfCategoriesState)) } - const searchRegExp = new RegExp(search, 'gi') - return Object.entries(getState(ComponentShelfCategoriesState)) .map(([category, items]) => { - const filteredItems = items.filter((item) => item.name.match(searchRegExp)?.length) + const filteredItems = items.filter((item) => item.name.toLowerCase().includes(search.toLowerCase())) return [category, filteredItems] as [string, Component[]] }) .filter(([_, items]) => !!items.length) diff --git a/packages/editor/src/components/hierarchy/HierarchyPanelContainer.tsx b/packages/editor/src/components/hierarchy/HierarchyPanelContainer.tsx index 01d0aebc96..01ea277fc5 100644 --- a/packages/editor/src/components/hierarchy/HierarchyPanelContainer.tsx +++ b/packages/editor/src/components/hierarchy/HierarchyPanelContainer.tsx @@ -108,9 +108,9 @@ function HierarchyPanelContents(props: { sceneURL: string; rootEntityUUID: Entit const nodeSearch: HeirarchyTreeNodeType[] = [] if (searchHierarchy.value.length > 0) { - const condition = new RegExp(searchHierarchy.value.toLowerCase()) + const searchString = searchHierarchy.value.toLowerCase() entityHierarchy.value.forEach((node) => { - if (node.entity && condition.test(getComponent(node.entity, NameComponent)?.toLowerCase() ?? '')) + if (node.entity && (getComponent(node.entity, NameComponent)?.toLowerCase() ?? '').includes(searchString)) nodeSearch.push(node) }) } diff --git a/packages/editor/src/components/prefabs/PrefabList.tsx b/packages/editor/src/components/prefabs/PrefabList.tsx index 669555166c..167984cde9 100644 --- a/packages/editor/src/components/prefabs/PrefabList.tsx +++ b/packages/editor/src/components/prefabs/PrefabList.tsx @@ -113,9 +113,8 @@ const usePrefabShelfCategories = (search: string) => { return getState(PrefabShelfState) } - const searchRegExp = new RegExp(search, 'gi') - - return getState(PrefabShelfState).filter(({ name }) => !!name.match(searchRegExp)?.length) + const searchString = search.toLowerCase() + return getState(PrefabShelfState).filter(({ name }) => name.toLowerCase().includes(searchString)) } export function PrefabList() { diff --git a/packages/editor/src/components/properties/ModelNodeEditor.tsx b/packages/editor/src/components/properties/ModelNodeEditor.tsx index de1010ca7c..46a9447bd4 100755 --- a/packages/editor/src/components/properties/ModelNodeEditor.tsx +++ b/packages/editor/src/components/properties/ModelNodeEditor.tsx @@ -33,13 +33,13 @@ import { ProjectState } from '@etherealengine/client-core/src/common/services/Pr import config from '@etherealengine/common/src/config' import { pathJoin } from '@etherealengine/common/src/utils/miscUtils' import { useComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { pathResolver } from '@etherealengine/engine/src/assets/functions/pathResolver' import { updateModelResource } from '@etherealengine/engine/src/assets/functions/resourceLoaderFunctions' import { recursiveHipsLookup } from '@etherealengine/engine/src/avatar/AvatarBoneMatching' import { getEntityErrors } from '@etherealengine/engine/src/scene/components/ErrorComponent' import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' import { getState, useState } from '@etherealengine/hyperflux' +import { STATIC_ASSET_REGEX } from '@etherealengine/common/src/regex' import { exportRelativeGLTF } from '../../functions/exportGLTF' import { EditorState } from '../../services/EditorServices' import BooleanInput from '../inputs/BooleanInput' @@ -69,10 +69,10 @@ export const ModelNodeEditor: EditorComponentType = (props) => { const editorState = getState(EditorState) const projectState = getState(ProjectState) const loadedProjects = useState(() => projectState.projects.map((project) => project.name)) - const srcProject = useState(() => pathResolver().exec(modelComponent.src.value)?.[1] ?? editorState.projectName!) + const srcProject = useState(() => STATIC_ASSET_REGEX.exec(modelComponent.src.value)?.[1] ?? editorState.projectName!) const getRelativePath = useCallback(() => { - const relativePath = pathResolver().exec(modelComponent.src.value)?.[2] + const relativePath = STATIC_ASSET_REGEX.exec(modelComponent.src.value)?.[2] if (!relativePath) { return 'assets/new-model' } else { diff --git a/packages/editor/src/components/toolbar/Toolbar2.tsx b/packages/editor/src/components/toolbar/Toolbar2.tsx index a8d03d1b8e..b6c649f3e7 100644 --- a/packages/editor/src/components/toolbar/Toolbar2.tsx +++ b/packages/editor/src/components/toolbar/Toolbar2.tsx @@ -23,13 +23,16 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ +import AddEditLocationModal from '@etherealengine/client-core/src/admin/components/locations/AddEditLocationModal' import { NotificationService } from '@etherealengine/client-core/src/common/services/NotificationService' import { PopoverState } from '@etherealengine/client-core/src/common/services/PopoverState' import { RouterState } from '@etherealengine/client-core/src/common/services/RouterService' import { useProjectPermissions } from '@etherealengine/client-core/src/user/useUserProjectPermission' import { useUserHasAccessHook } from '@etherealengine/client-core/src/user/userHasAccess' +import { locationPath } from '@etherealengine/common/src/schema.type.module' import { GLTFModifiedState } from '@etherealengine/engine/src/gltf/GLTFDocumentState' import { getMutableState, getState, useHookstate, useMutableState } from '@etherealengine/hyperflux' +import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks' import ContextMenu from '@etherealengine/ui/src/components/editor/layout/ContextMenu' import Button from '@etherealengine/ui/src/primitives/tailwind/Button' import { t } from 'i18next' @@ -113,11 +116,13 @@ export default function Toolbar() { const anchorEvent = useHookstate>(null) const anchorPosition = useHookstate({ left: 0, top: 0 }) - const { projectName, sceneName } = useMutableState(EditorState) + const { projectName, sceneName, sceneAssetID } = useMutableState(EditorState) const hasLocationWriteScope = useUserHasAccessHook('location:write') const permission = useProjectPermissions(projectName.value!) const hasPublishAccess = hasLocationWriteScope || permission?.type === 'owner' || permission?.type === 'editor' + const locationQuery = useFind(locationPath, { query: { sceneId: sceneAssetID.value } }) + const currentLocation = locationQuery.data.length === 1 ? locationQuery.data[0] : undefined return ( <> @@ -148,9 +153,19 @@ export default function Toolbar() { / {sceneName.value} - + {sceneAssetID.value && ( + + )} } diff --git a/packages/editor/src/functions/exportGLTF.ts b/packages/editor/src/functions/exportGLTF.ts index 57a001a609..c887037612 100644 --- a/packages/editor/src/functions/exportGLTF.ts +++ b/packages/editor/src/functions/exportGLTF.ts @@ -23,14 +23,14 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ +import { STATIC_ASSET_REGEX } from '@etherealengine/common/src/regex' import { Entity } from '@etherealengine/ecs/src/Entity' import exportModelGLTF from '@etherealengine/engine/src/assets/functions/exportModelGLTF' -import { pathResolver } from '@etherealengine/engine/src/assets/functions/pathResolver' import { uploadProjectFiles } from './assetFunctions' export default async function exportGLTF(entity: Entity, path: string) { - const [, pName, fileName] = pathResolver().exec(path)! + const [, pName, fileName] = STATIC_ASSET_REGEX.exec(path)! return exportRelativeGLTF(entity, pName, fileName) } diff --git a/packages/engine/src/assets/constants/LoaderConstants.ts b/packages/engine/src/assets/constants/LoaderConstants.ts index cfd7e1406d..cb344d3766 100755 --- a/packages/engine/src/assets/constants/LoaderConstants.ts +++ b/packages/engine/src/assets/constants/LoaderConstants.ts @@ -29,8 +29,6 @@ export const DEFAULT_LOD_DISTANCES = Object.freeze({ '2': 30 }) -export const LODS_REGEXP = new RegExp(/^(.*)_LOD(\d+)$/) - export const LOADER_STATUS = { LOADED: 0, LOADING: 1, diff --git a/packages/engine/src/assets/exporters/gltf/extensions/ImageRoutingExtension.ts b/packages/engine/src/assets/exporters/gltf/extensions/ImageRoutingExtension.ts index 8af2bc9d39..f9f569dc3e 100644 --- a/packages/engine/src/assets/exporters/gltf/extensions/ImageRoutingExtension.ts +++ b/packages/engine/src/assets/exporters/gltf/extensions/ImageRoutingExtension.ts @@ -28,8 +28,8 @@ import { Material, Object3D, Object3DEventMap, Texture } from 'three' import { pathJoin, relativePathTo } from '@etherealengine/common/src/utils/miscUtils' import { EntityUUID, UUIDComponent, getOptionalComponent } from '@etherealengine/ecs' +import { STATIC_ASSET_REGEX } from '@etherealengine/common/src/regex' import { SourceComponent } from '../../../../scene/components/SourceComponent' -import { pathResolver } from '../../../functions/pathResolver' import { GLTFExporterPlugin, GLTFWriter } from '../GLTFExporter' import { ExporterExtension } from './ExporterExtension' @@ -47,7 +47,7 @@ export default class ImageRoutingExtension extends ExporterExtension implements if (!materialEntity) return const src = getOptionalComponent(materialEntity, SourceComponent) if (!src) return - const resolvedPath = pathResolver().exec(src)! + const resolvedPath = STATIC_ASSET_REGEX.exec(src)! const projectDst = this.writer.options.projectName! let projectSrc = this.writer.options.projectName! let relativeSrc = './assets/' @@ -66,7 +66,7 @@ export default class ImageRoutingExtension extends ExporterExtension implements if (texture.image instanceof ImageBitmap) continue let oldURI = texture.userData.src if (!oldURI) { - const resolved = pathResolver().exec(texture.image.src)! + const resolved = STATIC_ASSET_REGEX.exec(texture.image.src)! const oldProject = resolved[1] const relativeOldURL = resolved[2] if (oldProject !== projectSrc) { diff --git a/packages/engine/src/assets/functions/pathResolver.ts b/packages/engine/src/assets/functions/pathResolver.ts index f311a7c9cc..8e55bef4ef 100644 --- a/packages/engine/src/assets/functions/pathResolver.ts +++ b/packages/engine/src/assets/functions/pathResolver.ts @@ -23,12 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -export function pathResolver() { - //const cacheRe = new RegExp(`(https://[^\\/]+)/projects/([^/]+)/(.*$)`) - const cacheRe = new RegExp(`.*/(?:projects|static-resources)/([^/]*)/((?:assets/|).*)`) - // 1: project name -- 2: internal path - return cacheRe -} +import { STATIC_ASSET_REGEX } from '@etherealengine/common/src/regex' export function getBasePath(path: string) { const regex = new RegExp(`(.*/(?:projects|static-resources)/[^/]*)`) @@ -40,11 +35,11 @@ export function getFileName(path: string) { } export function getRelativeURI(path: string) { - return pathResolver().exec(path)?.[2] ?? '' + return STATIC_ASSET_REGEX.exec(path)?.[2] ?? '' } export function getProjectName(path: string) { - return pathResolver().exec(path)?.[1] ?? '' + return STATIC_ASSET_REGEX.exec(path)?.[1] ?? '' } export function modelResourcesPath(modelName: string) { diff --git a/packages/engine/src/scene/functions/GLTFConversion.ts b/packages/engine/src/scene/functions/GLTFConversion.ts index f46ddf7119..8ddef230fa 100644 --- a/packages/engine/src/scene/functions/GLTFConversion.ts +++ b/packages/engine/src/scene/functions/GLTFConversion.ts @@ -31,6 +31,7 @@ import { sceneRelativePathIdentifier } from '@etherealengine/common/src/utils/pa import { EntityUUID, generateEntityUUID, SerializedComponentType, UUIDComponent } from '@etherealengine/ecs' import { TransformComponent } from '@etherealengine/spatial' +import { getCacheRegex } from '@etherealengine/common/src/regex' import { EntityJsonType, SceneJsonType } from '../types/SceneTypes' export const nodeToEntityJson = (node: any): EntityJsonType => { @@ -81,7 +82,7 @@ export const gltfToSceneJson = (gltf: any): SceneJsonType => { * @param mode 'encode' or 'decode' */ export const handleScenePaths = (gltf: GLTF.IGLTF, mode: 'encode' | 'decode') => { - const cacheRe = new RegExp(`${config.client.fileServer}\/projects`) + const cacheRe = getCacheRegex(config.client.fileServer) const symbolRe = /__\$project\$__/ const frontier = [...(gltf.scenes ?? []), ...(gltf.nodes ?? [])] while (frontier.length > 0) { diff --git a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts index 117bc147c5..1516505683 100644 --- a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts +++ b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts @@ -23,7 +23,7 @@ import { DistanceFromCameraComponent } from '@etherealengine/spatial/src/transfo import { TransformComponent } from '@etherealengine/spatial/src/transform/components/TransformComponent' import { isMobileXRHeadset } from '@etherealengine/spatial/src/xr/XRState' -import { pathResolver } from '../../../assets/functions/pathResolver' +import { STATIC_ASSET_REGEX } from '@etherealengine/common/src/regex' import { getGLTFAsync } from '../../../assets/functions/resourceLoaderHooks' import { InstancingComponent } from '../../components/InstancingComponent' import { ModelComponent } from '../../components/ModelComponent' @@ -66,8 +66,8 @@ export function updateModelVariant( const levelIndex = variantComponent.levels.findIndex((level) => level.metadata['device'] === targetDevice) if (levelIndex < 0) return const deviceVariant = variantComponent.levels[levelIndex] - const modelRelativePath = pathResolver().exec(modelComponent.src.value)?.[2] - const deviceRelativePath = deviceVariant ? pathResolver().exec(deviceVariant.src.value)?.[2] : '' + const modelRelativePath = STATIC_ASSET_REGEX.exec(modelComponent.src.value)?.[2] + const deviceRelativePath = deviceVariant ? STATIC_ASSET_REGEX.exec(deviceVariant.src.value)?.[2] : '' if (deviceVariant && modelRelativePath !== deviceRelativePath) { variantComponent.currentLevel.set(levelIndex) } diff --git a/packages/server-core/src/hooks/project-permission-authenticate.ts b/packages/server-core/src/hooks/project-permission-authenticate.ts index d526eb4222..1e7e8a332e 100755 --- a/packages/server-core/src/hooks/project-permission-authenticate.ts +++ b/packages/server-core/src/hooks/project-permission-authenticate.ts @@ -26,7 +26,7 @@ Ethereal Engine. All Rights Reserved. import { BadRequest, Forbidden } from '@feathersjs/errors' import { HookContext, Paginated } from '@feathersjs/feathers' -import { GITHUB_URL_REGEX } from '@etherealengine/common/src/constants/GitHubConstants' +import { GITHUB_URL_REGEX } from '@etherealengine/common/src/regex' import { projectPermissionPath, ProjectPermissionType @@ -88,9 +88,9 @@ export default (writeAccess) => { if (githubIdentityProvider.data.length === 0) throw new Forbidden('You are not authorized to access this project') const githubPathRegexExec = GITHUB_URL_REGEX.exec(projectRepoPath) if (!githubPathRegexExec) throw new BadRequest('Invalid project URL') - const split = githubPathRegexExec[2].split('/') + const split = githubPathRegexExec[1].split('/') const owner = split[0] - const repo = split[1].replace('.git', '') + const repo = split[1] const userRepoWriteStatus = await checkUserRepoWriteStatus(owner, repo, githubIdentityProvider.data[0].oauthToken) if (userRepoWriteStatus !== 200) throw new Forbidden('You are not authorized to access this project') } diff --git a/packages/server-core/src/media/storageprovider/s3.storage.ts b/packages/server-core/src/media/storageprovider/s3.storage.ts index d821d3d101..c88adce0fc 100755 --- a/packages/server-core/src/media/storageprovider/s3.storage.ts +++ b/packages/server-core/src/media/storageprovider/s3.storage.ts @@ -66,12 +66,14 @@ import S3BlobStore from 's3-blob-store' import { PassThrough, Readable } from 'stream' import { MULTIPART_CHUNK_SIZE, MULTIPART_CUTOFF_SIZE } from '@etherealengine/common/src/constants/FileSizeConstants' + import { - assetsRegex, - projectPublicRegex, - projectRegex, - projectThumbnailsRegex -} from '@etherealengine/common/src/constants/ProjectKeyConstants' + ASSETS_REGEX, + PROJECT_PUBLIC_REGEX, + PROJECT_REGEX, + PROJECT_THUMBNAIL_REGEX +} from '@etherealengine/common/src/regex' + import { FileBrowserContentType } from '@etherealengine/common/src/schemas/media/file-browser.schema' import config from '../../appconfig' @@ -112,7 +114,10 @@ const awsPath = './.aws/s3' const credentialsPath = `${awsPath}/credentials` export const getACL = (key: string) => - projectRegex.test(key) && !projectPublicRegex.test(key) && !projectThumbnailsRegex.test(key) && !assetsRegex.test(key) + PROJECT_REGEX.test(key) && + !PROJECT_PUBLIC_REGEX.test(key) && + !PROJECT_THUMBNAIL_REGEX.test(key) && + !ASSETS_REGEX.test(key) ? ObjectCannedACL.private : ObjectCannedACL.public_read diff --git a/packages/server-core/src/projects/project-permission/project-permission.hooks.ts b/packages/server-core/src/projects/project-permission/project-permission.hooks.ts index 62c40ad46d..1f43656183 100644 --- a/packages/server-core/src/projects/project-permission/project-permission.hooks.ts +++ b/packages/server-core/src/projects/project-permission/project-permission.hooks.ts @@ -23,7 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { INVITE_CODE_REGEX, USER_ID_REGEX } from '@etherealengine/common/src/constants/IdConstants' +import { INVITE_CODE_REGEX, USER_ID_REGEX } from '@etherealengine/common/src/regex' import { ProjectPermissionData, ProjectPermissionPatch, diff --git a/packages/server-core/src/projects/project/github-helper.ts b/packages/server-core/src/projects/project/github-helper.ts index 4ced03244d..2dc62fde52 100755 --- a/packages/server-core/src/projects/project/github-helper.ts +++ b/packages/server-core/src/projects/project/github-helper.ts @@ -31,7 +31,8 @@ import fs from 'fs' import fetch from 'node-fetch' import path from 'path' -import { GITHUB_PER_PAGE, GITHUB_URL_REGEX } from '@etherealengine/common/src/constants/GitHubConstants' +import { GITHUB_PER_PAGE } from '@etherealengine/common/src/constants/GitHubConstants' +import { GITHUB_URL_REGEX } from '@etherealengine/common/src/regex' import { apiJobPath } from '@etherealengine/common/src/schemas/cluster/api-job.schema' import { ProjectType, projectPath } from '@etherealengine/common/src/schemas/projects/project.schema' import { @@ -206,9 +207,9 @@ export const pushProject = async ( const githubPathRegexExec = GITHUB_URL_REGEX.exec(repoPath) if (!githubPathRegexExec) throw new BadRequest('Invalid Github URL') - const split = githubPathRegexExec[2].split('/') + const split = githubPathRegexExec[1].split('/') const owner = split[0] - const repo = split[1].replace('.git', '') + const repo = split[1] if (githubIdentityProvider.data.length === 0 || !githubIdentityProvider.data[0].oauthToken) throw new Forbidden('You must log out and log back in with Github to refresh the token, and then try again.') @@ -465,14 +466,14 @@ export const getGithubOwnerRepo = (url: string) => { error: 'invalidUrl', text: 'Project URL is not a valid GitHub URL, or the GitHub repo is private' } - const split = githubPathRegexExec[2].split('/') + const split = githubPathRegexExec[1].split('/') if (!split[0] || !split[1]) return { error: 'invalidUrl', text: 'Project URL is not a valid GitHub URL, or the GitHub repo is private' } const owner = split[0] - const repo = split[1].replace('.git', '') + const repo = split[1] return { owner, repo diff --git a/packages/server-core/src/projects/project/project-helper.ts b/packages/server-core/src/projects/project/project-helper.ts index 7e33907d62..2422671e61 100644 --- a/packages/server-core/src/projects/project/project-helper.ts +++ b/packages/server-core/src/projects/project/project-helper.ts @@ -44,7 +44,8 @@ import semver from 'semver' import { promisify } from 'util' import { AssetType } from '@etherealengine/common/src/constants/AssetType' -import { INSTALLATION_SIGNED_REGEX, PUBLIC_SIGNED_REGEX } from '@etherealengine/common/src/constants/GitHubConstants' +import { INSTALLATION_SIGNED_REGEX, PUBLIC_SIGNED_REGEX } from '@etherealengine/common/src/regex' + import { ManifestJson } from '@etherealengine/common/src/interfaces/ManifestJson' import { ProjectPackageJsonType } from '@etherealengine/common/src/interfaces/ProjectPackageJsonType' import { ResourcesJson } from '@etherealengine/common/src/interfaces/ResourcesJson' diff --git a/packages/server-core/src/projects/project/project.hooks.ts b/packages/server-core/src/projects/project/project.hooks.ts index 1395e29508..024b29b025 100644 --- a/packages/server-core/src/projects/project/project.hooks.ts +++ b/packages/server-core/src/projects/project/project.hooks.ts @@ -31,8 +31,8 @@ import fs from 'fs' import { Knex } from 'knex' import path from 'path' -import { GITHUB_URL_REGEX } from '@etherealengine/common/src/constants/GitHubConstants' import { ManifestJson } from '@etherealengine/common/src/interfaces/ManifestJson' +import { GITHUB_URL_REGEX } from '@etherealengine/common/src/regex' import { apiJobPath } from '@etherealengine/common/src/schemas/cluster/api-job.schema' import { staticResourcePath, StaticResourceType } from '@etherealengine/common/src/schemas/media/static-resource.schema' import { ProjectBuildUpdateItemType } from '@etherealengine/common/src/schemas/projects/project-build.schema' @@ -165,7 +165,7 @@ const ensurePushStatus = async (context: HookContext) => { allowedProjectGithubRepos.map(async (project) => { const regexExec = GITHUB_URL_REGEX.exec(project.repositoryPath) if (!regexExec) return { repositoryPath: '', name: '' } - const split = regexExec[2].split('/') + const split = regexExec[1].split('/') project.repositoryPath = `https://github.com/${split[0]}/${split[1]}` return project }) @@ -184,7 +184,7 @@ const ensurePushStatus = async (context: HookContext) => { repositoryPaths.push(`${url}.git`) const regexExec = GITHUB_URL_REGEX.exec(url) if (regexExec) { - const split = regexExec[2].split('/') + const split = regexExec[1].split('/') repositoryPaths.push(`git@github.com:${split[0]}/${split[1]}`) repositoryPaths.push(`git@github.com:${split[0]}/${split[1]}.git`) } @@ -360,9 +360,9 @@ const linkGithubToProject = async (context: HookContext) => { if (!githubPathRegexExec) throw new BadRequest('Invalid Github URL') if (githubIdentityProvider.data.length === 0) throw new Error('Must be logged in with GitHub to link a project to a GitHub repo') - const split = githubPathRegexExec[2].split('/') + const split = githubPathRegexExec[1].split('/') const org = split[0] - const repo = split[1].replace('.git', '') + const repo = split[1] const appOrgAccess = await checkAppOrgStatus(org, githubIdentityProvider.data[0].oauthToken) if (!appOrgAccess) throw new Forbidden( diff --git a/packages/server-core/src/setting/client-setting/client-setting.hooks.ts b/packages/server-core/src/setting/client-setting/client-setting.hooks.ts index a22cb1eeea..8724014398 100644 --- a/packages/server-core/src/setting/client-setting/client-setting.hooks.ts +++ b/packages/server-core/src/setting/client-setting/client-setting.hooks.ts @@ -29,6 +29,7 @@ import { iff, isProvider } from 'feathers-hooks-common' import path from 'path' import { invalidationPath } from '@etherealengine/common/src/schemas/media/invalidation.schema' + import { ClientSettingData, clientSettingDataValidator, @@ -69,11 +70,11 @@ const updateWebManifest = async (context: HookContext) => try { const webmanifestResponse = await storageProvider.getObject(webmanifestPath) const webmanifest = JSON.parse(webmanifestResponse.Body.toString('utf-8')) - context.data![0].startPath = data![0].startPath?.replace(/https:\/\//, '/') - const icon192px = /https:\/\//.test(data![0].icon192px!) + context.data![0].startPath = data![0].startPath?.replace('https://', '/') + const icon192px = data![0].icon192px!.startsWith('https://') ? data![0].icon192px : path.join('client', data![0].icon192px!) - const icon512px = /https:\/\//.test(data![0].icon512px!) + const icon512px = data![0].icon512px!.startsWith('https://') ? data![0].icon512px : path.join('client', data![0].icon512px!) webmanifest.name = data![0].title @@ -87,7 +88,7 @@ const updateWebManifest = async (context: HookContext) => const cacheDomain = storageProvider.getCacheDomain() webmanifest.icons = [ { - src: /https:\/\//.test(icon192px!) + src: icon192px!.startsWith('https://') ? icon192px : cacheDomain[cacheDomain.length - 1] === '/' && icon192px![0] === '/' ? `https://${cacheDomain}${icon192px?.slice(1)}` @@ -98,7 +99,7 @@ const updateWebManifest = async (context: HookContext) => type: getContentType(icon192px!) }, { - src: /https:\/\//.test(icon512px!) + src: icon512px!.startsWith('https://') ? icon512px : cacheDomain[cacheDomain.length - 1] === '/' && icon512px![0] === '/' ? `https://${cacheDomain}${icon512px?.slice(1)}` diff --git a/packages/spatial/src/common/functions/isAbsolutePath.ts b/packages/spatial/src/common/functions/isAbsolutePath.ts index cc6038ba8b..0d5520c2aa 100755 --- a/packages/spatial/src/common/functions/isAbsolutePath.ts +++ b/packages/spatial/src/common/functions/isAbsolutePath.ts @@ -23,7 +23,8 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -const isAbsolute = new RegExp(`(?:^[a-z][a-z0-9+.-]*:|\/\/)`) +import { ABSOLUTE_URL_PROTOCOL_REGEX } from '@etherealengine/common/src/regex' + export const isAbsolutePath = (path) => { - return isAbsolute.test(path) + return ABSOLUTE_URL_PROTOCOL_REGEX.test(path) } diff --git a/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx b/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx index 94799894b4..9c886ee9b2 100644 --- a/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx @@ -183,7 +183,7 @@ export const FileGridItem: React.FC = (props) => { color="text-[#375DAF]" /> -
{props.item.fullName}
+
{props.item.fullName}
) } diff --git a/packages/ui/src/components/editor/panels/Properties/elementList/index.tsx b/packages/ui/src/components/editor/panels/Properties/elementList/index.tsx index 5b4dab143c..808129520d 100644 --- a/packages/ui/src/components/editor/panels/Properties/elementList/index.tsx +++ b/packages/ui/src/components/editor/panels/Properties/elementList/index.tsx @@ -160,11 +160,11 @@ const useComponentShelfCategories = (search: string) => { return Object.entries(getState(ComponentShelfCategoriesState)) } - const searchRegExp = new RegExp(search, 'gi') + const searchString = search.toLowerCase() return Object.entries(getState(ComponentShelfCategoriesState)) .map(([category, items]) => { - const filteredItems = items.filter((item) => item.name.match(searchRegExp)?.length) + const filteredItems = items.filter((item) => item.name.toLowerCase().includes(searchString)) return [category, filteredItems] as [string, Component[]] }) .filter(([_, items]) => !!items.length) @@ -185,11 +185,11 @@ const usePrefabShelfCategories = (search: string): [string, PrefabShelfItem[]][] return Object.entries(prefabShelves) } - const searchRegExp = new RegExp(search, 'gi') + const searchString = search.toLowerCase() return Object.entries(prefabShelves) .map(([category, items]) => { - const filteredItems = items.filter((item) => item.name.match(searchRegExp)?.length) + const filteredItems = items.filter((item) => item.name.toLowerCase().includes(searchString)) return [category, filteredItems] as [string, PrefabShelfItem[]] }) .filter(([_, items]) => !!items.length) diff --git a/packages/ui/src/components/editor/properties/model/index.tsx b/packages/ui/src/components/editor/properties/model/index.tsx index 5b57f00647..e84a291d8a 100644 --- a/packages/ui/src/components/editor/properties/model/index.tsx +++ b/packages/ui/src/components/editor/properties/model/index.tsx @@ -31,13 +31,13 @@ import { Object3D, Scene } from 'three' import { ProjectState } from '@etherealengine/client-core/src/common/services/ProjectService' import config from '@etherealengine/common/src/config' +import { STATIC_ASSET_REGEX } from '@etherealengine/common/src/regex' import { pathJoin } from '@etherealengine/common/src/utils/miscUtils' import { useComponent } from '@etherealengine/ecs/src/ComponentFunctions' import ErrorPopUp from '@etherealengine/editor/src/components/popup/ErrorPopUp' import { EditorComponentType, commitProperty } from '@etherealengine/editor/src/components/properties/Util' import { exportRelativeGLTF } from '@etherealengine/editor/src/functions/exportGLTF' import { EditorState } from '@etherealengine/editor/src/services/EditorServices' -import { pathResolver } from '@etherealengine/engine/src/assets/functions/pathResolver' import { updateModelResource } from '@etherealengine/engine/src/assets/functions/resourceLoaderFunctions' import { recursiveHipsLookup } from '@etherealengine/engine/src/avatar/AvatarBoneMatching' import { getEntityErrors } from '@etherealengine/engine/src/scene/components/ErrorComponent' @@ -67,10 +67,10 @@ export const ModelNodeEditor: EditorComponentType = (props) => { const editorState = getState(EditorState) const projectState = getState(ProjectState) const loadedProjects = useState(() => projectState.projects.map((project) => project.name)) - const srcProject = useState(() => pathResolver().exec(modelComponent.src.value)?.[1] ?? editorState.projectName!) + const srcProject = useState(() => STATIC_ASSET_REGEX.exec(modelComponent.src.value)?.[1] ?? editorState.projectName!) const getRelativePath = useCallback(() => { - const relativePath = pathResolver().exec(modelComponent.src.value)?.[2] + const relativePath = STATIC_ASSET_REGEX.exec(modelComponent.src.value)?.[2] if (!relativePath) { return 'assets/new-model' } else { diff --git a/packages/xrui/core/serialization/generateEmbeddedCSS.ts b/packages/xrui/core/serialization/generateEmbeddedCSS.ts index 08e5186ae8..cd3f1ea0a5 100644 --- a/packages/xrui/core/serialization/generateEmbeddedCSS.ts +++ b/packages/xrui/core/serialization/generateEmbeddedCSS.ts @@ -23,6 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ +import { CSS_URL_REGEX } from '@etherealengine/common/src/regex/index' import { WebRenderer } from '../WebRenderer' import { getEmbeddedDataURL } from './getEmbeddedDataURL' @@ -39,14 +40,12 @@ export async function generateEmbeddedCSS(url: string, css: string): Promise[] // Add classes for psuedo-classes - css = css.replace(new RegExp(':hover', 'g'), WebRenderer.attributeCSS(WebRenderer.HOVER_ATTRIBUTE)) - css = css.replace(new RegExp(':active', 'g'), WebRenderer.attributeCSS(WebRenderer.ACTIVE_ATTRIBUTE)) - css = css.replace(new RegExp(':focus', 'g'), WebRenderer.attributeCSS(WebRenderer.FOCUS_ATTRIBUTE)) - css = css.replace(new RegExp(':target', 'g'), WebRenderer.attributeCSS(WebRenderer.TARGET_ATTRIBUTE)) - - // Replace all urls in the css - const regEx = RegExp(/(@import.*?["']([^"']+)["'].*?|url\((?!['"]?(?:data):)['"]?([^'"\)]*)['"]?\))/gi) - while ((found = regEx.exec(css))) { + css = css.replaceAll(':hover', WebRenderer.attributeCSS(WebRenderer.HOVER_ATTRIBUTE)) + css = css.replaceAll(':active', WebRenderer.attributeCSS(WebRenderer.ACTIVE_ATTRIBUTE)) + css = css.replaceAll(':focus', WebRenderer.attributeCSS(WebRenderer.FOCUS_ATTRIBUTE)) + css = css.replaceAll(':target', WebRenderer.attributeCSS(WebRenderer.TARGET_ATTRIBUTE)) + + while ((found = CSS_URL_REGEX.exec(css))) { const isCSSImport = !!found[2] const accept = isCSSImport ? 'type/css' : undefined const resourceURL = found[2] || found[3]