From fbf23b6e18e99b9570687cbaef05b3f92de9337c Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 6 May 2024 18:58:19 +0200 Subject: [PATCH] Add switches to enable some more privacy, fix all errors in osmAuth --- assets/layers/usersettings/usersettings.json | 35 ++++ package-lock.json | 29 +-- package.json | 2 +- src/Logic/Osm/OsmConnection.ts | 179 ++++++++---------- src/Logic/State/UserRelatedState.ts | 13 +- .../ThemeConfig/Json/LayerConfigJson.ts | 2 +- 6 files changed, 128 insertions(+), 132 deletions(-) diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index 35629f2fa..5b0d14090 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -484,6 +484,41 @@ } ] }, + { + "id": "more_privacy_theme_override", + "mappings": [ + { + "if": "__featureSwitchMorePrivacy=true", + "then": { + "en": "This theme is sensitive. Making changes will not indicate if you were nearby explicitly." + } + } + ] + }, + { + "id": "more_privacy", + "question": + { + "en": "When making changes, should a rough indication be given how far away you were from the object?" + }, + "questionHint": { + "en": "If you make a change to one or more objects and you enabled your location, a rough indication of where you made will be saved: it is indicated if you were closer then 25m, 500m, 5km or further away then 5km. This helps mappers understand your context when making changes, but gives an indication of where you were at this time. " + }, + "mappings": [ + { + "if": "mapcomplete-more_privacy=yes", + "then": { + "en": "When making changes to OpenStreetMap, do not indicate how far away you were from the changed objects." + } + }, + { + "if": "mapcomplete-more_privacy=no", + "then": { + "en": "When making changes to OpenStreetMap, roughly indicate how far away you were from the changed objects. This helps other contributors to understand how you made the change" + } + } + ] + }, { "id": "mangrove-keys", "render": { diff --git a/package-lock.json b/package-lock.json index 90e49eaa1..91fbe3b3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,7 @@ "monaco-editor": "^0.46.0", "npm": "^10.7.0", "opening_hours": "^3.6.0", - "osm-auth": "^2.2.0", + "osm-auth": "^2.5.0", "osmtogeojson": "^3.0.0-beta.5", "panzoom": "^9.4.3", "papaparse": "^5.3.1", @@ -14631,13 +14631,11 @@ } }, "node_modules/osm-auth": { - "version": "2.2.0", - "license": "ISC", - "dependencies": { - "store": "~2.0.12" - }, + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/osm-auth/-/osm-auth-2.5.0.tgz", + "integrity": "sha512-w3NnYbt+0PIih2Kwr1sLfQWehdLbcA3gZNJhX4VOBfeRtvm30iZA3nURphuZDokZ8Kmdv4LWB+AiIng2b+KvIA==", "engines": { - "node": ">=16" + "node": ">=18.18" } }, "node_modules/osm-polygon-features": { @@ -17081,13 +17079,6 @@ "version": "3.3.2", "license": "MIT" }, - "node_modules/store": { - "version": "2.0.12", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/stream-to-string": { "version": "1.2.1", "license": "MIT", @@ -29202,10 +29193,9 @@ } }, "osm-auth": { - "version": "2.2.0", - "requires": { - "store": "~2.0.12" - } + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/osm-auth/-/osm-auth-2.5.0.tgz", + "integrity": "sha512-w3NnYbt+0PIih2Kwr1sLfQWehdLbcA3gZNJhX4VOBfeRtvm30iZA3nURphuZDokZ8Kmdv4LWB+AiIng2b+KvIA==" }, "osm-polygon-features": { "version": "0.9.2" @@ -30803,9 +30793,6 @@ "std-env": { "version": "3.3.2" }, - "store": { - "version": "2.0.12" - }, "stream-to-string": { "version": "1.2.1", "requires": { diff --git a/package.json b/package.json index 997a19a74..442956f86 100644 --- a/package.json +++ b/package.json @@ -173,7 +173,7 @@ "monaco-editor": "^0.46.0", "npm": "^10.7.0", "opening_hours": "^3.6.0", - "osm-auth": "^2.2.0", + "osm-auth": "^2.5.0", "osmtogeojson": "^3.0.0-beta.5", "panzoom": "^9.4.3", "papaparse": "^5.3.1", diff --git a/src/Logic/Osm/OsmConnection.ts b/src/Logic/Osm/OsmConnection.ts index 3e458f4b2..d7fae19cb 100644 --- a/src/Logic/Osm/OsmConnection.ts +++ b/src/Logic/Osm/OsmConnection.ts @@ -1,4 +1,3 @@ -// @ts-ignore import { osmAuth } from "osm-auth" import { Store, Stores, UIEventSource } from "../UIEventSource" import { OsmPreferences } from "./OsmPreferences" @@ -6,7 +5,18 @@ import { Utils } from "../../Utils" import { LocalStorageSource } from "../Web/LocalStorageSource" import { AuthConfig } from "./AuthConfig" import Constants from "../../Models/Constants" -import OSMAuthInstance = OSMAuth.OSMAuthInstance + +interface OsmUserInfo { + id: number + display_name: string + account_created: string + description: string + contributor_terms: { agreed: boolean } + roles: [] + changesets: { count: number } + traces: { count: number } + blocks: { received: { count: number; active: number } } +} export default class UserDetails { public loggedIn = false @@ -31,7 +41,7 @@ export default class UserDetails { export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable" export class OsmConnection { - public auth: OSMAuthInstance + public auth: osmAuth public userDetails: UIEventSource public isLoggedIn: Store public gpxServiceIsOnline: UIEventSource = new UIEventSource( @@ -49,7 +59,7 @@ export class OsmConnection { private readonly _dryRun: Store private readonly fakeUser: boolean private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [] - private readonly _iframeMode: Boolean | boolean + private readonly _iframeMode: boolean private readonly _singlePage: boolean private isChecking = false private readonly _doCheckRegularly @@ -99,20 +109,19 @@ export class OsmConnection { ud.languages = ["en"] this.loadingStatus.setData("logged-in") } - const self = this this.UpdateCapabilities() this.isLoggedIn = this.userDetails.map( (user) => user.loggedIn && - (self.apiIsOnline.data === "unknown" || self.apiIsOnline.data === "online"), + (this.apiIsOnline.data === "unknown" || this.apiIsOnline.data === "online"), [this.apiIsOnline] ) this.isLoggedIn.addCallback((isLoggedIn) => { - if (self.userDetails.data.loggedIn == false && isLoggedIn == true) { + if (this.userDetails.data.loggedIn == false && isLoggedIn == true) { // We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do // This means someone attempted to toggle this; so we attempt to login! - self.AttemptLogin() + this.AttemptLogin() } }) @@ -120,17 +129,16 @@ export class OsmConnection { this.updateAuthObject() if (!this.fakeUser) { - self.CheckForMessagesContinuously() + this.CheckForMessagesContinuously() } this.preferencesHandler = new OsmPreferences(this.auth, this, this.fakeUser) if (options.oauth_token?.data !== undefined) { console.log(options.oauth_token.data) - const self = this this.auth.bootstrapToken(options.oauth_token.data, (err, result) => { console.log("Bootstrap token called back", err, result) - self.AttemptLogin() + this.AttemptLogin() }) options.oauth_token.setData(undefined) @@ -142,15 +150,15 @@ export class OsmConnection { } } - public GetPreference( + public GetPreference( key: string, defaultValue: string = undefined, options?: { documentation?: string prefix?: string } - ): UIEventSource { - return this.preferencesHandler.GetPreference(key, defaultValue, options) + ): UIEventSource { + return >this.preferencesHandler.GetPreference(key, defaultValue, options) } public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { @@ -192,7 +200,7 @@ export class OsmConnection { console.log("AttemptLogin called, but ignored as fakeUser is set") return } - const self = this + console.log("Trying to log in...") this.updateAuthObject() @@ -202,33 +210,33 @@ export class OsmConnection { this.auth.xhr( { method: "GET", - path: "/api/0.6/user/details", + path: "/api/0.6/user/details" }, - function (err, details: XMLDocument) { + (err, details: XMLDocument) => { if (err != null) { console.log("Could not login due to:", err) - self.loadingStatus.setData("error") + this.loadingStatus.setData("error") if (err.status == 401) { console.log("Clearing tokens...") // Not authorized - our token probably got revoked - self.auth.logout() - self.LogOut() + this.auth.logout() + this.LogOut() } else { console.log("Other error. Status:", err.status) - self.apiIsOnline.setData("unreachable") + this.apiIsOnline.setData("unreachable") } return } if (details == null) { - self.loadingStatus.setData("error") + this.loadingStatus.setData("error") return } // details is an XML DOM of user details - let userInfo = details.getElementsByTagName("user")[0] + const userInfo = details.getElementsByTagName("user")[0] - let data = self.userDetails.data + const data = this.userDetails.data data.loggedIn = true console.log("Login completed, userinfo is ", userInfo) data.name = userInfo.getAttribute("display_name") @@ -261,18 +269,18 @@ export class OsmConnection { data.home = { lat: lat, lon: lon } } - self.loadingStatus.setData("logged-in") + this.loadingStatus.setData("logged-in") const messages = userInfo .getElementsByTagName("messages")[0] .getElementsByTagName("received")[0] data.unreadMessages = parseInt(messages.getAttribute("unread")) data.totalMessages = parseInt(messages.getAttribute("count")) - self.userDetails.ping() - for (const action of self._onLoggedIn) { - action(self.userDetails.data) + this.userDetails.ping() + for (const action of this._onLoggedIn) { + action(this.userDetails.data) } - self._onLoggedIn = [] + this._onLoggedIn = [] } ) } @@ -289,11 +297,11 @@ export class OsmConnection { public async interact( path: string, method: "GET" | "POST" | "PUT" | "DELETE", - header?: Record, + header?: Record, content?: string, allowAnonymous: boolean = false ): Promise { - let connection: OSMAuthInstance = this.auth + const connection: osmAuth = this.auth if (allowAnonymous && !this.auth.authenticated()) { const possibleResult = await Utils.downloadAdvanced( `${this.Backend()}/api/0.6/${path}`, @@ -310,15 +318,13 @@ export class OsmConnection { return new Promise((ok, error) => { connection.xhr( - { + { method, - options: { - header, - }, + headers: header, content, - path: `/api/0.6/${path}`, + path: `/api/0.6/${path}` }, - function (err, response) { + function(err, response) { if (err !== null) { error(err) } else { @@ -329,32 +335,32 @@ export class OsmConnection { }) } - public async post( + public async post( path: string, content?: string, - header?: Record, + header?: Record, allowAnonymous: boolean = false - ): Promise { - return await this.interact(path, "POST", header, content, allowAnonymous) + ): Promise { + return await this.interact(path, "POST", header, content, allowAnonymous) } - public async put( + public async put( path: string, content?: string, - header?: Record - ): Promise { - return await this.interact(path, "PUT", header, content) + header?: Record + ): Promise { + return await this.interact(path, "PUT", header, content) } public async get( path: string, - header?: Record, + header?: Record, allowAnonymous: boolean = false ): Promise { return await this.interact(path, "GET", header, undefined, allowAnonymous) } - public closeNote(id: number | string, text?: string): Promise { + public closeNote(id: number | string, text?: string): Promise { let textSuffix = "" if ((text ?? "") !== "") { textSuffix = "?text=" + encodeURIComponent(text) @@ -362,17 +368,17 @@ export class OsmConnection { if (this._dryRun.data) { console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) return new Promise((ok) => { - ok() + ok("") }) } return this.post(`notes/${id}/close${textSuffix}`) } - public reopenNote(id: number | string, text?: string): Promise { + public reopenNote(id: number | string, text?: string): Promise { if (this._dryRun.data) { console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) - return new Promise((ok) => { - ok() + return new Promise(resolve => { + resolve("") }) } let textSuffix = "" @@ -398,7 +404,7 @@ export class OsmConnection { "notes.json", content, { - "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, true ) @@ -439,7 +445,7 @@ export class OsmConnection { file: gpx, description: options.description, tags: options.labels?.join(",") ?? "", - visibility: options.visibility, + visibility: options.visibility } if (!contents.description) { @@ -447,9 +453,9 @@ export class OsmConnection { } const extras = { file: - '; filename="' + + "; filename=\"" + (options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) + - '"\r\nContent-Type: application/gpx+xml', + "\"\r\nContent-Type: application/gpx+xml" } const boundary = "987654" @@ -457,7 +463,7 @@ export class OsmConnection { let body = "" for (const key in contents) { body += "--" + boundary + "\r\n" - body += 'Content-Disposition: form-data; name="' + key + '"' + body += "Content-Disposition: form-data; name=\"" + key + "\"" if (extras[key] !== undefined) { body += extras[key] } @@ -468,7 +474,7 @@ export class OsmConnection { const response = await this.post("gpx/create", body, { "Content-Type": "multipart/form-data; boundary=" + boundary, - "Content-Length": body.length, + "Content-Length": ""+body.length }) const parsed = JSON.parse(response) console.log("Uploaded GPX track", parsed) @@ -491,9 +497,9 @@ export class OsmConnection { { method: "POST", - path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`, + path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}` }, - function (err, _) { + function(err) { if (err !== null) { error(err) } else { @@ -508,7 +514,7 @@ export class OsmConnection { * To be called by land.html */ public finishLogin(callback: (previousURL: string) => void) { - this.auth.authenticate(function () { + this.auth.authenticate(function() { // Fully authed at this point console.log("Authentication successful!") const previousLocation = LocalStorageSource.Get("location_before_login") @@ -517,28 +523,6 @@ export class OsmConnection { } private updateAuthObject() { - let pwaStandAloneMode = false - try { - if (Utils.runningFromConsole) { - pwaStandAloneMode = true - } else if ( - window.matchMedia("(display-mode: standalone)").matches || - window.matchMedia("(display-mode: fullscreen)").matches - ) { - pwaStandAloneMode = true - } - } catch (e) { - console.warn( - "Detecting standalone mode failed", - e, - ". Assuming in browser and not worrying furhter" - ) - } - const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage - - // In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway... - // Same for an iframe... - this.auth = new osmAuth({ client_id: this._oauth_config.oauth_client_id, url: this._oauth_config.url, @@ -546,23 +530,22 @@ export class OsmConnection { redirect_uri: Utils.runningFromConsole ? "https://mapcomplete.org/land.html" : window.location.protocol + "//" + window.location.host + "/land.html", - singlepage: true, - auto: true, + singlepage: true, // We always use 'singlePage', it is the most stable - including in PWA + auto: true }) } private CheckForMessagesContinuously() { - const self = this if (this.isChecking) { return } - Stores.Chronic(3 * 1000).addCallback((_) => { - if (!(self.apiIsOnline.data === "unreachable" || self.apiIsOnline.data === "offline")) { + Stores.Chronic(3 * 1000).addCallback(() => { + if (!(this.apiIsOnline.data === "unreachable" || this.apiIsOnline.data === "offline")) { return } try { console.log("Api is offline - trying to reconnect...") - self.AttemptLogin() + this.AttemptLogin() } catch (e) { console.log("Could not login due to", e) } @@ -571,10 +554,10 @@ export class OsmConnection { if (!this._doCheckRegularly) { return } - Stores.Chronic(60 * 5 * 1000).addCallback((_) => { - if (self.isLoggedIn.data) { + Stores.Chronic(60 * 5 * 1000).addCallback(() => { + if (this.isLoggedIn.data) { try { - self.AttemptLogin() + this.AttemptLogin() } catch (e) { console.log("Could not login due to", e) } @@ -592,19 +575,9 @@ export class OsmConnection { }) } - private readonly _userInfoCache: Record = {} + private readonly _userInfoCache: Record = {} - public async getInformationAboutUser(id: number): Promise<{ - id: number - display_name: string - account_created: string - description: string - contributor_terms: { agreed: boolean } - roles: [] - changesets: { count: number } - traces: { count: number } - blocks: { received: { count: number; active: number } } - }> { + public async getInformationAboutUser(id: number): Promise { if (id === undefined) { return undefined } diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index 7892f2691..b1d0b4161 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -43,6 +43,7 @@ export default class UserRelatedState { public readonly fixateNorth: UIEventSource public readonly a11y: UIEventSource public readonly homeLocation: FeatureSource + public readonly morePrivacy: UIEventSource /** * The language as saved into the preferences of the user, if logged in. * Note that this is _different_ from the languages a user can set via the osm.org interface here: https://www.openstreetmap.org/preferences @@ -106,12 +107,12 @@ export default class UserRelatedState { }) ) this.language = this.osmConnection.GetPreference("language") - this.showTags = >this.osmConnection.GetPreference("show_tags") - this.showCrosshair = >this.osmConnection.GetPreference("show_crosshair") - this.fixateNorth = >this.osmConnection.GetPreference("fixate-north") - this.a11y = >( - this.osmConnection.GetPreference("a11y") - ) + this.showTags = this.osmConnection.GetPreference("show_tags") + this.showCrosshair = this.osmConnection.GetPreference("show_crosshair") + this.fixateNorth = this.osmConnection.GetPreference("fixate-north") + this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no") + + this.a11y = this.osmConnection.GetPreference("a11y") this.mangroveIdentity = new MangroveIdentity( this.osmConnection.GetLongPreference("identity", "mangrove"), diff --git a/src/Models/ThemeConfig/Json/LayerConfigJson.ts b/src/Models/ThemeConfig/Json/LayerConfigJson.ts index 00bf57e4e..3d32196c0 100644 --- a/src/Models/ThemeConfig/Json/LayerConfigJson.ts +++ b/src/Models/ThemeConfig/Json/LayerConfigJson.ts @@ -564,5 +564,5 @@ export interface LayerConfigJson { * ifunset: Write 'change_within_x_m' as usual and if GPS is enabled * iftrue: Do not write 'change_within_x_m' and do not indicate that this was done by survey */ - enableMorePrivacy: boolean + enableMorePrivacy?: boolean }