Skip to content

Commit

Permalink
#9877 Fixes basic authentication form show for streetview (#9890)
Browse files Browse the repository at this point in the history
  • Loading branch information
offtherailz authored Jan 17, 2024
1 parent ffe9cca commit a666db2
Show file tree
Hide file tree
Showing 17 changed files with 214 additions and 63 deletions.
28 changes: 25 additions & 3 deletions web/client/components/map/openlayers/__tests__/Layer-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3036,16 +3036,16 @@ describe('Openlayers layer', () => {

describe('WFS', () => {
// this function create a WFS layer with the given options.
const createWFSLayerTest = (options, done, onRenderComplete = () => {}) => {
const createWFSLayerTest = (options, done, onRenderComplete = () => {}, checkFeatures = true) => {
let layer;
map.on('rendercomplete', () => {
if (layer.layer.getSource().getFeatures().length > 0) {
if (layer.layer.getSource().getFeatures().length > 0 && checkFeatures) {
const f = layer.layer.getSource().getFeatures()[0];
expect(f.getGeometry().getCoordinates()[0]).toBe(SAMPLE_FEATURE_COLLECTION.features[0].geometry.coordinates[0]);
expect(f.getGeometry().getCoordinates()[1]).toBe(SAMPLE_FEATURE_COLLECTION.features[0].geometry.coordinates[1]);
onRenderComplete(layer);
done();
}
done();
});
// first render
layer = ReactDOM.render(<OpenlayersLayer
Expand Down Expand Up @@ -3166,6 +3166,28 @@ describe('Openlayers layer', () => {
setCredentials("TEST_SOURCE", undefined);
});
});
it('test security basic authentication with no credentials', (done) => {
mockAxios.onPost().reply(({

}) => {
done("should not be called"); // request should not be performed to avoid basic authentication popup
return [200, SAMPLE_FEATURE_COLLECTION];
});
setCredentials("TEST_SOURCE", undefined);
createWFSLayerTest({
type: 'wfs',
visibility: true,
url: 'SAMPLE_URL',
name: 'osm:vector_tile',
serverType: ServerTypes.NO_VENDOR,
security: {
type: 'basic',
sourceId: 'TEST_SOURCE'
}
}, done, () => {
setCredentials("TEST_SOURCE", undefined);
}, false);
});
});
});
it('should apply native ol min and max resolution on vector layer', () => {
Expand Down
35 changes: 23 additions & 12 deletions web/client/components/map/openlayers/plugins/WFSLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import { optionsToVendorParams } from '../../../../utils/VendorParamsUtils';
import { getCredentials } from '../../../../utils/SecurityUtils';
import { needsReload } from '../../../../utils/WFSLayerUtils';
import { applyDefaultStyleToVectorLayer } from '../../../../utils/StyleUtils';
const needsCredentials = (options) => {
const security = options?.security || {};
const {type, sourceId} = security;
const {username, password} = getCredentials(sourceId) ?? {};
return type?.toLowerCase?.() === "basic" && (!username || !password);
};
const getConfig = (options) => {
const security = options?.security || {};
const config = {};
Expand Down Expand Up @@ -55,25 +61,29 @@ const createLoader = (source, options) => (extent, resolution, projection) => {
source.dispatchEvent('vectorerror');
};
if (options.serverType === ServerTypes.NO_VENDOR) {
if (options?.strategy === 'bbox') {

if (needsCredentials(options)) {
req = new Promise((resolve, reject) => {reject();});
} else {
if (options?.strategy === 'bbox') {
// here bbox filter is
const [left, bottom, right, top] = extent;
const [left, bottom, right, top] = extent;

filters = [{
spatialField: {
operation: 'BBOX',
geometry: {
projection: proj,
extent: [[left, bottom, right, top]] // use array because bbox is buggy
filters = [{
spatialField: {
operation: 'BBOX',
geometry: {
projection: proj,
extent: [[left, bottom, right, top]] // use array because bbox is buggy
}
}
}
}];
}];
}
req = getFeatureLayer(options, {filters, proj}, getConfig(options));
}
req = getFeatureLayer(options, {filters, proj}, getConfig(options));
} else {
const params = optionsToVendorParams(options);
const config = getConfig(options);

req = getFeature(options.url, options.name, {
// bbox: extent.join(',') + ',' + proj,
outputFormat: "application/json",
Expand All @@ -82,6 +92,7 @@ const createLoader = (source, options) => (extent, resolution, projection) => {
...params
}, config);
}

req.then(response => {
if (response.status === 200) {
source.addFeatures(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
setPov, SET_POV,
configure, CONFIGURE,
reset, RESET,
toggleStreetView
toggleStreetView,
updateStreetViewLayer, UPDATE_STREET_VIEW_LAYER

} from '../streetView';
import { TOGGLE_CONTROL } from '../../../../actions/controls';
Expand Down Expand Up @@ -91,4 +92,11 @@ describe('StreetView actions', () => {
});
ret(dispatch, getState);
});
it('updateStreetViewLayer', () => {
const updates = {_v_: 1};
const ret = updateStreetViewLayer(updates);
expect(ret).toExist();
expect(ret.type).toBe(UPDATE_STREET_VIEW_LAYER);
expect(ret.updates).toBe(updates);
});
});
13 changes: 12 additions & 1 deletion web/client/plugins/StreetView/actions/streetView.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const SET_LOCATION = "STREET_VIEW:SET_LOCATION";
export const SET_POV = "STREET_VIEW:SET_POV";
export const CONFIGURE = "STREET_VIEW:CONFIGURE";
export const RESET = "STREET_VIEW:RESET";
export const UPDATE_STREET_VIEW_LAYER = "STREET_VIEW:UPDATE_STREET_VIEW_LAYER";

export function setAPILoading(loading) {
return {
Expand Down Expand Up @@ -57,7 +58,17 @@ export function toggleStreetView() {

};
}

/**
* Updates the properties of the street view layer.
* @param {object} updates properties to update
* @returns {object}
*/
export function updateStreetViewLayer(updates) {
return {
type: UPDATE_STREET_VIEW_LAYER,
updates
};
}
export function setLocation(location) {
return {
type: SET_LOCATION,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,30 @@ import { Form, Button, ControlLabel, FormControl, Glyphicon } from 'react-bootst
import tooltip from '../../../../components/misc/enhancers/tooltip';
const ButtonT = tooltip(Button);
/**
* Component to insert Smart API Credentials
* Component to insert Smart API Credentials.
* If showCredentialsForm is false, it shows only a button to open the form.
* When showCredentialsForm is true, it shows the form to insert credentials.
* @prop {function} setCredentials function to set credentials
* @prop {object} credentials object with username and password
* @prop {boolean} showCredentialsForm show form
* @prop {function} setShowCredentialsForm function to set showCredentialsForm
* @returns {JSX.Element} The rendered component
*/
export default ({setCredentials = () => {}, credentials}) => {
const [hasCredentials, setHasCredentials] = useState(credentials?.username && credentials?.password);
export default ({setCredentials = () => {}, credentials, showCredentialsForm, setShowCredentialsForm = () => {}}) => {
const [username, setUsername] = useState(credentials?.username || '');
const [password, setPassword] = useState(credentials?.password || '');
const onSubmit = () => {
setCredentials({username, password});
setHasCredentials(true);
setShowCredentialsForm(false);
};
if (hasCredentials) {
if (!showCredentialsForm) {
// show only button to reset credentials.
return (<div style={{textAlign: "right"}}>
<ButtonT
style={{marginRight: 10, border: "none", background: "none"}}
tooltipId="streetView.cyclomedia.changeCredentials"
onClick={() => {
setCredentials(null);
setHasCredentials(false);
setShowCredentialsForm(true);
}}><Glyphicon glyph="1-user-mod" /></ButtonT>
</div>);
}
Expand All @@ -38,6 +40,12 @@ export default ({setCredentials = () => {}, credentials}) => {
<FormControl type="password" value={password} onChange={e => setPassword(e.target.value)}/>
<div className="street-view-credentials-form-buttons">
<Button disabled={!username || !password} onClick={() => onSubmit()}><Message msgId="streetView.cyclomedia.submit" /></Button>
{
credentials?.username && credentials?.password && <Button onClick={() => {
setCredentials({username: credentials.username, password: credentials.password});
setShowCredentialsForm(false);
} }><Message msgId="cancel" /></Button>
}
</div>
</Form>
</div>);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@ import { Alert, Button } from 'react-bootstrap';
import CyclomediaCredentials from './Credentials';
import EmptyStreetView from '../EmptyStreetView';

const isInvalidCredentials = (error) => {
return error?.message?.indexOf?.("code 401");
};
/**
* Parses the error message to show to the user in the alert an user friendly message
* @private
* @param {object|string} error the error to parse
* @returns {string|JSX.Element} the error message
*/
const getErrorMessage = (error) => {
if (error?.indexOf?.("init::Loading user info failed with status code 401") >= 0) {
return <Message msgId="streetView.cyclomedia.invalidCredentials" />;
if (isInvalidCredentials(error) >= 0) {
return <Message msgId="streetView.cyclomedia.errors.invalidCredentials" />;
}

return error?.message ?? "Unknown error";
};

Expand Down Expand Up @@ -72,10 +76,11 @@ const EmptyView = ({initializing, initialized, StreetSmartApi, mapPointVisible})
* @param {function} props.setLocation the function to call when the location changes. It receives the new location as parameter (an object with `latLng` and `properties` properties)
* @param {boolean} props.mapPointVisible true if the map point are visible at the current level of zoom. It is used to show a message to zoom in when the map point are not visible.
* @param {object} props.providerSettings the settings of the provider. It contains the `StreetSmartApiURL` property that is the URL of the Cyclomedia API
* @param {function} props.refreshLayer the function to call to refresh the layer. It is used to refresh the layer when the credentials are changed.
* @returns {JSX.Element} the component rendering
*/

const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLocation = () => {}, mapPointVisible, providerSettings = {}}) => {
const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLocation = () => {}, mapPointVisible, providerSettings = {}, refreshLayer = () => {}}) => {
const StreetSmartApiURL = providerSettings?.StreetSmartApiURL ?? "https://streetsmart.cyclomedia.com/api/v23.7/StreetSmartApi.js";
const scripts = providerSettings?.scripts ?? `
<script type="text/javascript" src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
Expand All @@ -102,15 +107,27 @@ const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLo
// gets the credentials from the storage
const initialCredentials = getStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE);
const [credentials, setCredentials] = useState(initialCredentials);
const [showCredentialsForm, setShowCredentialsForm] = useState(!credentials?.username || !credentials?.password); // determines to show the credentials form
const {username, password} = credentials ?? {};
const resetCredentials = () => {
if (getStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE)) {
setStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE, undefined);
}
};

const srs = 'EPSG:4326';
// this EPSG:7791 enables the measurement tool (where present), but coordinates of the click
// are in the srs named, so we need to convert them to EPSG:4326. Actually definition is not in place
// and have to be implemented
// it enables also the oblique tool, but we have to implement the click on that point too.

/**
* Utility function to open an image in street smart viewer (it must be called after the API is initialized)
* @param {string} query query for StreetSmartApi.open
* @param {string} srs SRS for StreetSmartApi.open
* @returns {Promise} a promise that resolves with the panoramaViewer
*/
const openImage = (query, srs) => {
const openImage = (query) => {
const viewerType = StreetSmartApi.ViewerType.PANORAMA;
const options = {
viewerType: viewerType,
Expand Down Expand Up @@ -140,27 +157,39 @@ const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLo
password,
apiKey,
loginOauth: false,
srs: 'EPSG:4326',
srs: srs,
locale: 'en-us',
...initOptions
}).then(function() {
setInitializing(false);
setInitialized(true);
setError(null);
}).catch(function(err) {
setInitializing(false);
setError(err);
console.error('Cyclomedia API: init: error: ' + err);
if (err) {console.error('Cyclomedia API: init: error: ' + err);}
});
return () => {
try {
setInitialized(false);
StreetSmartApi?.destroy?.({targetElement});
} catch (e) {
console.error(e);
}

};
}, [StreetSmartApi, username, password, apiKey, reload]);

// update credentials in the storage (for layer and memorization)
useEffect(() => {
const invalid = isInvalidCredentials(error);
if (initialized && username && password && !invalid && initialized) {
setStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE, credentials);
refreshLayer();
} else {
resetCredentials();
refreshLayer();
}
}, [initialized, username, password, username, password, error, initialized]);
const changeView = (_, {detail} = {}) => {
const {yaw: heading, pitch} = detail ?? {};
setPov({heading, pitch});
Expand All @@ -187,7 +216,7 @@ const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLo
let panoramaViewer;
let viewChangeHandler;
let recordingClickHandler;
openImage(imageId, 'EPSG:4326')
openImage(imageId)
.then((result) => {
if (result && result[0]) {
panoramaViewer = result[0];
Expand All @@ -210,14 +239,11 @@ const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLo
};
}, [StreetSmartApi, initialized, imageId]);

// handle view state
const hasCredentials = username && password;
// flag to show the credentials form
const showCredentialsForm = !hasCredentials;
// flag to show the panorama viewer
const showPanoramaViewer = StreetSmartApi && initialized && imageId && !showCredentialsForm && !error;
// flag to show the empty view
const showEmptyView = !showCredentialsForm && !showPanoramaViewer && !error;
const showEmptyView = initializing || !showCredentialsForm && !showPanoramaViewer && !error;
const showError = error && !showCredentialsForm && !showPanoramaViewer && !initializing;

// create the iframe content
const srcDoc = `<html>
Expand Down Expand Up @@ -245,10 +271,11 @@ const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLo
return (<>
{<CyclomediaCredentials
key="credentials"
showCredentialsForm={showCredentialsForm}
setShowCredentialsForm={setShowCredentialsForm}
credentials={credentials}
setCredentials={(newCredentials) => {
setCredentials(newCredentials);
setStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE, newCredentials);
}}/>}
{showEmptyView ? <EmptyView key="empty-view" StreetSmartApi={StreetSmartApi} style={style} initializing={initializing} initialized={initialized} mapPointVisible={mapPointVisible}/> : null}
<iframe key="iframe" ref={viewer} onLoad={() => {
Expand All @@ -257,7 +284,7 @@ const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLo
}} style={{ ...style, display: showPanoramaViewer ? 'block' : 'none'}} srcDoc={srcDoc}>

</iframe>
<Alert bsStyle="danger" style={{...style, textAlign: 'center', alignContent: 'center', display: error ? 'block' : 'none'}} key="error">
<Alert bsStyle="danger" style={{...style, textAlign: 'center', alignContent: 'center', display: showError ? 'block' : 'none'}} key="error">
<Message msgId="streetView.cyclomedia.errorOccurred" />
{getErrorMessage(error)}
{initialized ? <div><Button
Expand Down
Loading

0 comments on commit a666db2

Please sign in to comment.