diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..438b774c70 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,96 @@ +# 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: [ "master", "develop" ] + paths-ignore: + - 'tests/jest/fileTransformer.js' + pull_request: + branches: [ "master", "develop" ] + paths-ignore: + - 'tests/jest/fileTransformer.js' + schedule: + - cron: '36 8 * * 0' + +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: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', '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 + + # 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/ReleaseNotes.md b/ReleaseNotes.md index 520ef8374d..1ff1283011 100755 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,5 +1,16 @@ # Release Notes +## 2.14.1 + +For a full list of changes see: +https://github.com/oskariorg/oskari-frontend/milestone/52?closed=1 + +Fixed an issue where layers with URL templates couldn't be added with the admin UI. + +Removed postinstall script that did not work properly when dev-mode was not used. It was replaced with a Babel-plugin that can handle Cesium quirks. + +Updated dompurify 2.3.6 -> 2.5.8 + ## 2.14.0 For a full list of changes see: diff --git a/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.js b/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.js index f648974040..f4f06593ba 100644 --- a/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.js +++ b/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.js @@ -19,9 +19,7 @@ export const cleanUrl = (url) => { keysToDelete.forEach((key) => urlObj.searchParams.delete(key)); const parts = urlObj.toString().split('://'); - if (parts.length > 1) { - return parts[1]; - } - - return urlObj.toString(); + const retValString = parts.length > 1 ? parts[1] : urlObj.toString(); + const decoded = decodeURIComponent(retValString); + return decoded; }; diff --git a/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.test.js b/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.test.js index 885e0ff91d..e9684d8a4f 100644 --- a/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.test.js +++ b/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.test.js @@ -59,5 +59,13 @@ describe('ServiceUrlInputHelper Tests ', () => { const url = 'www.com/'; expect(cleanUrl(url)).toBe(url); }); + + it('should NOT encode URL params', () => { + const url = 'avoin-karttakuva.maanmittauslaitos.fi/kiinteisto-avoin/tiles/wmts/1.0.0/kiinteistojaotus/default/v3/ETRS-TM35FIN/{z}/{y}/{x}.pbf'; + expect(cleanUrl(url)).toBe(url); + + const url2 = 'www.com/?first=1&SECOND=2&thiRd=3'; + expect(cleanUrl(url2)).toBe(url2); + }); }); }); diff --git a/bundles/catalogue/metadata/resources/locale/sv.js b/bundles/catalogue/metadata/resources/locale/sv.js index 9afb137983..0744d2db44 100644 --- a/bundles/catalogue/metadata/resources/locale/sv.js +++ b/bundles/catalogue/metadata/resources/locale/sv.js @@ -10,7 +10,7 @@ Oskari.registerLocalization({ "basic": "Grundläggande information", "inspire": "Inspire metadata", "jhs": "ISO 19115 metadata", - "quality": "Data kvalität", + "quality": "Datakvalitet", "actions": "Handlingar" }, "actions": { diff --git a/bundles/catalogue/metadataflyout/resources/locale/sv.js b/bundles/catalogue/metadataflyout/resources/locale/sv.js index e150c51221..b37f2dda04 100644 --- a/bundles/catalogue/metadataflyout/resources/locale/sv.js +++ b/bundles/catalogue/metadataflyout/resources/locale/sv.js @@ -16,7 +16,7 @@ Oskari.registerLocalization({ "abstract": "Grundläggande information", "inspire": "Inspire metadata", "jhs": "ISO 19115 metadata", - "quality": "Data kvalität", + "quality": "Datakvalitet", "actions": "Handlingar", "xml": "ISO 19139 XML fil", "coverage": { diff --git a/bundles/framework/publisher2/handler/PanelGeneralInfoHandler.js b/bundles/framework/publisher2/handler/PanelGeneralInfoHandler.js new file mode 100644 index 0000000000..675a8516bb --- /dev/null +++ b/bundles/framework/publisher2/handler/PanelGeneralInfoHandler.js @@ -0,0 +1,89 @@ +import { StateHandler, controllerMixin } from 'oskari-ui/util'; +import { PUBLISHER_BUNDLE_ID } from '../view/PublisherSideBarHandler'; + +class UIHandler extends StateHandler { + constructor () { + super(); + this.state = { + name: null, + domain: null, + language: null + }; + } + + init (data) { + const { name, domain, language } = data?.metadata || {}; + this.updateState({ + name: name || null, + domain: domain || null, + language: language || Oskari.getLang() + }); + } + + getValues () { + return { + metadata: { + ...this.getState() + } + }; + }; + + onChange (key, value) { + const { oldState } = this.getState(); + const newState = { + ...oldState + }; + newState[key] = value; + this.updateState({ + ...newState + }); + } + + validate () { + let errors = []; + const { name, domain } = this.state; + errors = errors.concat(this.validateName(name)); + errors = errors.concat(this.validateDomain(domain)); + return errors; + } + + validateName (value) { + const errors = []; + const sanitizedValue = Oskari.util.sanitize(value); + if (!value || !value.trim().length) { + errors.push({ + field: name, + error: Oskari.getMsg(PUBLISHER_BUNDLE_ID, 'BasicView.error.name') + }); + return errors; + } + if (sanitizedValue !== value) { + errors.push({ + field: name, + error: Oskari.getMsg(PUBLISHER_BUNDLE_ID, 'BasicView.error.nameIllegalCharacters') + }); + return errors; + } + return errors; + } + + validateDomain (name, value) { + const errors = []; + if (value && value.indexOf('://') !== -1) { + errors.push({ + field: name, + error: Oskari.getMsg(PUBLISHER_BUNDLE_ID, 'BasicView.error.domainStart') + }); + return errors; + } + return errors; + } +} + +const wrapped = controllerMixin(UIHandler, [ + 'validate', + 'getValues', + 'onChange' +]); + +export { wrapped as PanelGeneralInfoHandler }; diff --git a/bundles/framework/publisher2/handler/PanelMapPreviewHandler.js b/bundles/framework/publisher2/handler/PanelMapPreviewHandler.js new file mode 100644 index 0000000000..8a84e66e21 --- /dev/null +++ b/bundles/framework/publisher2/handler/PanelMapPreviewHandler.js @@ -0,0 +1,160 @@ +import { StateHandler, controllerMixin } from 'oskari-ui/util'; +import { PUBLISHER_BUNDLE_ID } from '../view/PublisherSideBarHandler'; + +export const CUSTOM_MAP_SIZE_ID = 'custom'; +const MAP_SIZE_FILL_ID = 'fill'; +export const MAP_SIZES = [{ + id: MAP_SIZE_FILL_ID, + width: '', + height: '', + selected: true, // default option + valid: true +}, { + id: 'small', + width: 580, + height: 387, + valid: true +}, { + id: 'medium', + width: 700, + height: 600, + valid: true +}, { + id: 'large', + width: 1240, + height: 700, + valid: true +}, { + id: CUSTOM_MAP_SIZE_ID, + valid: true +}]; + +export const CUSTOM_MAP_SIZE_LIMITS = { + minWidth: 30, + minHeight: 20, + maxWidth: 4000, + maxHeight: 2000 +}; + +class UIHandler extends StateHandler { + constructor () { + super(); + this.mapmodule = Oskari.getSandbox().findRegisteredModuleInstance('MainMapModule'); + this.state = { + id: MAP_SIZE_FILL_ID + }; + } + + init (data) { + const selectedId = data?.metadata?.preview || MAP_SIZE_FILL_ID; + const selectedMapSize = MAP_SIZES.find(size => size.id === selectedId); + if (selectedId === CUSTOM_MAP_SIZE_ID) { + selectedMapSize.width = data?.metadata?.size?.width || ''; + selectedMapSize.height = data?.metadata?.size?.height || ''; + } + this.updateMapSize(selectedMapSize); + } + + updateMapSize (mapSize) { + this.updateState({ + ...mapSize + }); + if (mapSize.valid) { + this.adjustDataContainer(); + } + } + + /** + * @method adjustDataContainer + * This horrific thing is what sets the left panel components, container and map size. + */ + adjustDataContainer () { + const selectedSize = this.getSelectedMapSize(); + const size = selectedSize.valid ? selectedSize : this.getActiveMapSize(); + + const mapDiv = this.mapmodule.getMapEl(); + mapDiv.width(size.width || '100%'); + mapDiv.height(size.height || '100%'); + } + + /** + * @private @method getActiveMapSize + * Returns an object containing the active map size. + * This will differ from selected size if selected size is invalid. + * + * @return {Object} size + */ + getActiveMapSize () { + const mapDiv = this.mapmodule.getMapEl(); + return { + width: mapDiv.width(), + height: mapDiv.height() + }; + } + + /** + * Stop panel. + * @method stop + * @public + **/ + stop () { + // restore "fill" as default size setting + this.updateMapSize(MAP_SIZES.find(size => size.id === MAP_SIZE_FILL_ID)); + + window.setTimeout(() => { + // calculate new sizes AFTER the publisher panel has been removed from page + // otherwise the publisher panel that we have while stopping is taking up space + // from the map and the map size is calculated wrong + this.adjustDataContainer(); + }, 200); + } + + getValues () { + const selected = this.getSelectedMapSize(); + const values = { + metadata: { + preview: selected.id + } + }; + + if (!isNaN(parseInt(selected.width)) && !isNaN(parseInt(selected.height))) { + values.metadata.size = { + width: selected.width, + height: selected.height + }; + } + return values; + } + + validate () { + const errors = []; + if (!this.getSelectedMapSize().valid) { + errors.push({ + field: 'size', + error: Oskari.getMsg(PUBLISHER_BUNDLE_ID, 'BasicView.error.size', CUSTOM_MAP_SIZE_LIMITS) + }); + } + return errors; + } + + /** + * @private @method getSelectedMapSize + * Returns an object containing the user seleted/set map size and the corresponding size option + * + * @return {Object} size + */ + getSelectedMapSize () { + return { + ...this.state + }; + } +} + +const wrapped = controllerMixin(UIHandler, [ + 'validate', + 'getValues', + 'getSelectedMapSize', + 'updateMapSize' +]); + +export { wrapped as PanelMapPreviewHandler }; diff --git a/bundles/framework/publisher2/instance.js b/bundles/framework/publisher2/instance.js index 33d73b6f12..2232fb03cd 100755 --- a/bundles/framework/publisher2/instance.js +++ b/bundles/framework/publisher2/instance.js @@ -315,7 +315,9 @@ Oskari.clazz.define('Oskari.mapframework.bundle.publisher2.PublisherBundleInstan // call set enabled before rendering the panels (avoid duplicate "normal map plugins") me.publisher.setEnabled(true); - me.publisher.render(root); + const publisherDiv = jQuery('
'); + root.prepend(publisherDiv); + me.publisher.render(publisherDiv); } else { Oskari.setLang(me.oskariLang); if (me.publisher) { diff --git a/bundles/framework/publisher2/resources/locale/en.js b/bundles/framework/publisher2/resources/locale/en.js index 95fa5c84c8..6f146c38b0 100755 --- a/bundles/framework/publisher2/resources/locale/en.js +++ b/bundles/framework/publisher2/resources/locale/en.js @@ -241,7 +241,7 @@ Oskari.registerLocalization( "size": "The map size is invalid. The width should be from {minWidth} to {maxWidth} pixels and the height from {minHeight} to {maxHeight} pixels.", "domain": "The website is required. Please type an address and try again.", "domainStart": "The website is invalid. Please type an address without http or www prefixes and try again.", - "name": "The map name is required. Plese type a name and try again.", + "name": "The map name is required. Please type a name and try again.", "nohelp": "The user guide is not available.", "saveFailed": "The embedded map could not be saved.", "nameIllegalCharacters": "The map name contains illegal characters (e.g. html-tags). Please correct the name and try again.", diff --git a/bundles/framework/publisher2/view/CollapseContent.jsx b/bundles/framework/publisher2/view/CollapseContent.jsx new file mode 100644 index 0000000000..b0eccfcff9 --- /dev/null +++ b/bundles/framework/publisher2/view/CollapseContent.jsx @@ -0,0 +1,16 @@ +import React, { useEffect, useState } from 'react'; +import { Collapse } from 'oskari-ui'; +import { PropTypes } from 'prop-types'; + +export const CollapseContent = ({ controller }) => { + const [items, setItems] = useState(controller.getCollapseItems()); + useEffect(() => { + controller.addStateListener(() => { setItems(controller.getCollapseItems()); }); + }, []); + + return