diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..ec454e49122 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,100 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '20 11 * * 2' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..034e8480320 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/examples/package-lock.json b/examples/package-lock.json index 5d2ad205b92..9861f7237bf 100644 --- a/examples/package-lock.json +++ b/examples/package-lock.json @@ -43,7 +43,7 @@ }, "..": { "name": "playcanvas", - "version": "2.8.0-dev.0", + "version": "2.12.0-beta.5", "dev": true, "license": "MIT", "dependencies": { @@ -591,6 +591,7 @@ "version": "8.11.3", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1735,6 +1736,7 @@ "version": "8.57.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4347,6 +4349,7 @@ "version": "4.13.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.5" }, @@ -5293,6 +5296,7 @@ "version": "5.4.3", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6930,6 +6934,7 @@ "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -7005,6 +7010,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8050,7 +8056,8 @@ "version": "0.0.1367902", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/doctrine": { "version": "2.1.0", @@ -8288,6 +8295,7 @@ "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -9512,10 +9520,11 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -9732,7 +9741,8 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.33.0.tgz", "integrity": "sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ms": { "version": "2.1.3", @@ -10077,6 +10087,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -10258,6 +10269,7 @@ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10271,6 +10283,7 @@ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10468,6 +10481,7 @@ "integrity": "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.6" }, @@ -10596,7 +10610,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.1", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { diff --git a/examples/src/static/index.html b/examples/src/static/index.html index 82f0d7b17f5..a61aa4fba24 100644 --- a/examples/src/static/index.html +++ b/examples/src/static/index.html @@ -11,6 +11,31 @@
+ + +
+

World Clocks

+
+
+
New York
+
--:--:--
+
+
+
London
+
--:--:--
+
+
+
Tokyo
+
--:--:--
+
+
+
Local Time
+
--:--:--
+
+
+
+ + diff --git a/examples/src/static/scripts/main.js b/examples/src/static/scripts/main.js new file mode 100644 index 00000000000..d678fa8db7b --- /dev/null +++ b/examples/src/static/scripts/main.js @@ -0,0 +1,55 @@ +// World Clock Feature +// Updates clocks for different time zones every second + +/** + * Updates all world clocks with current time + */ +function updateWorldClocks() { + const now = new Date(); + + // Time zone configurations + const timeZones = { + 'clock-newyork': 'America/New_York', + 'clock-london': 'Europe/London', + 'clock-tokyo': 'Asia/Tokyo', + 'clock-local': undefined // Local time zone + }; + + // Update each clock + Object.keys(timeZones).forEach((clockId) => { + const element = document.getElementById(clockId); + if (element) { + const timeZone = timeZones[clockId]; + const options = { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + timeZone: timeZone + }; + + try { + const timeString = now.toLocaleTimeString('en-US', options); + element.textContent = timeString; + } catch (error) { + console.error(`Error updating clock ${clockId}:`, error); + element.textContent = 'Error'; + } + } + }); +} + +// Initialize clocks when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeClocks); +} else { + initializeClocks(); +} + +function initializeClocks() { + // Update clocks immediately + updateWorldClocks(); + + // Update clocks every second + setInterval(updateWorldClocks, 1000); +} diff --git a/examples/src/static/styles.css b/examples/src/static/styles.css index 2304318ef5d..a1995854239 100644 --- a/examples/src/static/styles.css +++ b/examples/src/static/styles.css @@ -775,3 +775,60 @@ body { .pcui-slider > .pcui-numeric-input { flex: 1.5 !important; } + +/* World Clocks Widget */ +#world-clocks { + position: fixed; + bottom: 1rem; + right: 1rem; + background-color: rgba(17, 24, 39, 0.9); + border-radius: 0.5rem; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + padding: 1.5rem; + backdrop-filter: blur(4px); + border: 1px solid #374151; +} + +#world-clocks h2 { + font-size: 1.5rem; + font-weight: 700; + color: #fff; + margin: 0 0 1rem 0; + text-align: center; + border-bottom: 1px solid #374151; + padding-bottom: 0.5rem; +} + +.clocks-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.clock-item { + background-color: #1f2937; + border-radius: 0.5rem; + padding: 1rem; + text-align: center; + transition: background-color 0.15s ease-in-out; +} + +.clock-item:hover { + background-color: #374151; +} + +.clock-item .clock-label { + font-size: 0.875rem; + color: #9ca3af; + margin-bottom: 0.25rem; +} + +.clock-item .clock-time { + font-size: 1.25rem; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} + +.clock-time.blue { color: #60a5fa; } +.clock-time.green { color: #34d399; } +.clock-time.purple { color: #a78bfa; } +.clock-time.orange { color: #fb923c; } diff --git a/package-lock.json b/package-lock.json index 116dcb92553..781198afe17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3500,9 +3500,9 @@ "optional": true }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -4420,9 +4420,9 @@ "optional": true }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/scripts/utils/wb-data-scrub.js b/scripts/utils/wb-data-scrub.js new file mode 100644 index 00000000000..0f44d890150 --- /dev/null +++ b/scripts/utils/wb-data-scrub.js @@ -0,0 +1,247 @@ +// How to use: +// - Add this script to an entity +// - Configure the scrubbing rules via attributes +// - Call scrubData() method to clean/sanitize data objects +// - Listen to 'dataScrubbed' event to get the result +// +// Example: +// var scrubber = entity.script.wbDataScrumb; +// var cleanData = scrubber.scrubData(dirtyData); +var WbDataScrumb = pc.createScript('wbDataScrumb'); + +WbDataScrumb.attributes.add('removeNullValues', { + title: 'Remove Null Values', + description: 'If enabled, null and undefined values will be removed from objects', + type: 'boolean', + default: true +}); + +WbDataScrumb.attributes.add('removeEmptyStrings', { + title: 'Remove Empty Strings', + description: 'If enabled, empty strings will be removed from objects', + type: 'boolean', + default: false +}); + +WbDataScrumb.attributes.add('trimStrings', { + title: 'Trim Strings', + description: 'If enabled, string values will be trimmed of whitespace', + type: 'boolean', + default: true +}); + +WbDataScrumb.attributes.add('removeEmptyArrays', { + title: 'Remove Empty Arrays', + description: 'If enabled, empty arrays will be removed from objects', + type: 'boolean', + default: false +}); + +WbDataScrumb.attributes.add('sanitizeHtml', { + title: 'Sanitize HTML', + description: 'If enabled, HTML tags will be stripped from strings', + type: 'boolean', + default: false +}); + +WbDataScrumb.attributes.add('maxDepth', { + title: 'Max Depth', + description: 'Maximum depth to traverse nested objects (prevents infinite recursion)', + type: 'number', + default: 10, + min: 1, + max: 100 +}); + +// initialize code called once per entity +WbDataScrumb.prototype.initialize = function () { + this._htmlTagRegex = /<[^>]*>/g; +}; + +/** + * Scrubs/sanitizes a data value according to configured rules. + * + * Plain objects and arrays are traversed recursively and their primitive + * properties are scrubbed based on the configured options. Values such as + * functions, Date instances, RegExp objects, DOM nodes, and other non-plain + * objects are not modified and are returned as-is. + * + * @param {*} data - The data to scrub (object, array, or primitive). + * @param {number} [depth] - Current recursion depth (for internal use only). + * @returns {*} The scrubbed data. + */ +WbDataScrumb.prototype.scrubData = function (data, depth) { + depth = depth || 0; + + // Prevent infinite recursion + if (depth > this.maxDepth) { + console.warn('WbDataScrumb: Maximum depth reached, returning data as-is'); + return data; + } + + // Handle null/undefined + if (data === null || data === undefined) { + return this.removeNullValues ? undefined : data; + } + + // Handle strings + if (typeof data === 'string') { + var result = data; + + // Trim whitespace + if (this.trimStrings) { + result = result.trim(); + } + + // Sanitize HTML + if (this.sanitizeHtml) { + result = result.replace(this._htmlTagRegex, ''); + } + + // Remove empty strings + if (this.removeEmptyStrings && result === '') { + return undefined; + } + + return result; + } + + // Handle arrays + if (Array.isArray(data)) { + var scrubbedArray = []; + + for (var i = 0; i < data.length; i++) { + var scrubbedItem = this.scrubData(data[i], depth + 1); + if (scrubbedItem !== undefined) { + scrubbedArray.push(scrubbedItem); + } + } + + // Remove empty arrays if configured + if (this.removeEmptyArrays && scrubbedArray.length === 0) { + return undefined; + } + + return scrubbedArray; + } + + // Handle objects + if (typeof data === 'object') { + var scrubbedObject = {}; + + for (var key in data) { + if (data.hasOwnProperty(key)) { + var scrubbedValue = this.scrubData(data[key], depth + 1); + + // Only add the property if the value is not undefined + if (scrubbedValue !== undefined) { + scrubbedObject[key] = scrubbedValue; + } + } + } + + return scrubbedObject; + } + + // Return primitives as-is (numbers, booleans, etc.) + return data; +}; + +/** + * Scrubs data and fires an event with the result + * @param {*} data - The data to scrub + * @param {string} eventName - Optional event name (defaults to 'dataScrubbed') + * @returns {*} The scrubbed data + */ +WbDataScrumb.prototype.scrubAndNotify = function (data, eventName) { + var scrubbed = this.scrubData(data); + this.entity.fire(eventName || 'dataScrubbed', scrubbed); + return scrubbed; +}; + +/** + * Deeply clones a value, preserving Dates and handling circular references. + * Note: Functions and non-enumerable properties are copied by reference. + * @param {*} value - The value to clone + * @param {WeakMap} [seen] - Internal map to handle circular references + * @returns {*} A deep clone of the provided value + */ +WbDataScrumb.prototype._deepClone = function (value, seen) { + // Primitives and null are returned as-is + if (value === null || typeof value !== 'object') { + return value; + } + + // Initialize the WeakMap for tracking circular references + if (!seen) { + seen = new WeakMap(); + } + + // Return existing clone if we've already seen this object + if (seen.has(value)) { + return seen.get(value); + } + + // Preserve Date instances + if (value instanceof Date) { + return new Date(value.getTime()); + } + + var cloned; + + // Handle arrays + if (Array.isArray(value)) { + cloned = []; + seen.set(value, cloned); + for (var i = 0; i < value.length; i++) { + cloned[i] = this._deepClone(value[i], seen); + } + return cloned; + } + + // Handle plain objects + cloned = {}; + seen.set(value, cloned); + var keys = Object.keys(value); + for (var k = 0; k < keys.length; k++) { + var key = keys[k]; + cloned[key] = this._deepClone(value[key], seen); + } + + return cloned; +}; + +/** + * Creates a copy of the data before scrubbing (non-destructive) + * @param {*} data - The data to scrub + * @returns {*} A scrubbed copy of the data + */ +WbDataScrumb.prototype.scrubCopy = function (data) { + // Deep clone the data first using a robust cloning method + var copy = this._deepClone(data); + return this.scrubData(copy); +}; + +/** + * Validates that data meets basic cleanliness requirements + * @param {*} data - The data to validate + * @returns {boolean} True if data passes validation + */ +WbDataScrumb.prototype.validate = function (data) { + // Check for null/undefined if they should be removed + if (this.removeNullValues && (data === null || data === undefined)) { + return false; + } + + // Check for empty strings if they should be removed + if (this.removeEmptyStrings && data === '') { + return false; + } + + // Check for empty arrays if they should be removed + if (this.removeEmptyArrays && Array.isArray(data) && data.length === 0) { + return false; + } + + return true; +};