forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			259 lines
		
	
	
	
		
			8.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			259 lines
		
	
	
	
		
			8.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { UIEventSource } from "../UIEventSource"
 | 
						|
import UserDetails, { OsmConnection } from "./OsmConnection"
 | 
						|
import { Utils } from "../../Utils"
 | 
						|
import { DomEvent } from "leaflet"
 | 
						|
import preventDefault = DomEvent.preventDefault
 | 
						|
 | 
						|
export class OsmPreferences {
 | 
						|
    public preferences = new UIEventSource<Record<string, string>>({}, "all-osm-preferences")
 | 
						|
    private readonly preferenceSources = new Map<string, UIEventSource<string>>()
 | 
						|
    private auth: any
 | 
						|
    private userDetails: UIEventSource<UserDetails>
 | 
						|
    private longPreferences = {}
 | 
						|
 | 
						|
    constructor(auth, osmConnection: OsmConnection) {
 | 
						|
        this.auth = auth
 | 
						|
        this.userDetails = osmConnection.userDetails
 | 
						|
        const self = this
 | 
						|
        osmConnection.OnLoggedIn(() => self.UpdatePreferences())
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * OSM preferences can be at most 255 chars
 | 
						|
     * @param key
 | 
						|
     * @param prefix
 | 
						|
     * @constructor
 | 
						|
     */
 | 
						|
    public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
 | 
						|
        if (this.longPreferences[prefix + key] !== undefined) {
 | 
						|
            return this.longPreferences[prefix + key]
 | 
						|
        }
 | 
						|
 | 
						|
        const source = new UIEventSource<string>(undefined, "long-osm-preference:" + prefix + key)
 | 
						|
        this.longPreferences[prefix + key] = source
 | 
						|
 | 
						|
        const allStartWith = prefix + key + "-combined"
 | 
						|
        // Gives the number of combined preferences
 | 
						|
        const length = this.GetPreference(allStartWith + "-length", "", "")
 | 
						|
 | 
						|
        if ((allStartWith + "-length").length > 255) {
 | 
						|
            throw (
 | 
						|
                "This preference key is too long, it has " +
 | 
						|
                key.length +
 | 
						|
                " characters, but at most " +
 | 
						|
                (255 - "-length".length - "-combined".length - prefix.length) +
 | 
						|
                " characters are allowed"
 | 
						|
            )
 | 
						|
        }
 | 
						|
 | 
						|
        const self = this
 | 
						|
        source.addCallback((str) => {
 | 
						|
            if (str === undefined || str === "") {
 | 
						|
                return
 | 
						|
            }
 | 
						|
            if (str === null) {
 | 
						|
                console.error("Deleting " + allStartWith)
 | 
						|
                let count = parseInt(length.data)
 | 
						|
                for (let i = 0; i < count; i++) {
 | 
						|
                    // Delete all the preferences
 | 
						|
                    self.GetPreference(allStartWith + "-" + i, "", "").setData("")
 | 
						|
                }
 | 
						|
                self.GetPreference(allStartWith + "-length", "", "").setData("")
 | 
						|
                return
 | 
						|
            }
 | 
						|
 | 
						|
            let i = 0
 | 
						|
            while (str !== "") {
 | 
						|
                if (str === undefined || str === "undefined") {
 | 
						|
                    throw "Long pref became undefined?"
 | 
						|
                }
 | 
						|
                if (i > 100) {
 | 
						|
                    throw "This long preference is getting very long... "
 | 
						|
                }
 | 
						|
                self.GetPreference(allStartWith + "-" + i, "", "").setData(str.substr(0, 255))
 | 
						|
                str = str.substr(255)
 | 
						|
                i++
 | 
						|
            }
 | 
						|
            length.setData("" + i) // We use I, the number of preference fields used
 | 
						|
        })
 | 
						|
 | 
						|
        function updateData(l: number) {
 | 
						|
            if (Object.keys(self.preferences.data).length === 0) {
 | 
						|
                // The preferences are still empty - they are not yet updated, so we delay updating for now
 | 
						|
                return
 | 
						|
            }
 | 
						|
            const prefsCount = Number(l)
 | 
						|
            if (prefsCount > 100) {
 | 
						|
                throw "Length to long"
 | 
						|
            }
 | 
						|
            let str = ""
 | 
						|
            for (let i = 0; i < prefsCount; i++) {
 | 
						|
                const key = allStartWith + "-" + i
 | 
						|
                if (self.preferences.data[key] === undefined) {
 | 
						|
                    console.warn(
 | 
						|
                        "Detected a broken combined preference:",
 | 
						|
                        key,
 | 
						|
                        "is undefined",
 | 
						|
                        self.preferences
 | 
						|
                    )
 | 
						|
                }
 | 
						|
                str += self.preferences.data[key] ?? ""
 | 
						|
            }
 | 
						|
 | 
						|
            source.setData(str)
 | 
						|
        }
 | 
						|
 | 
						|
        length.addCallback((l) => {
 | 
						|
            updateData(Number(l))
 | 
						|
        })
 | 
						|
        this.preferences.addCallbackAndRun((_) => {
 | 
						|
            updateData(Number(length.data))
 | 
						|
        })
 | 
						|
 | 
						|
        return source
 | 
						|
    }
 | 
						|
 | 
						|
    public GetPreference(
 | 
						|
        key: string,
 | 
						|
        defaultValue: string = undefined,
 | 
						|
        prefix: string = "mapcomplete-"
 | 
						|
    ): UIEventSource<string> {
 | 
						|
        if (key.startsWith(prefix) && prefix !== "") {
 | 
						|
            console.trace(
 | 
						|
                "A preference was requested which has a duplicate prefix in its key. This is probably a bug"
 | 
						|
            )
 | 
						|
        }
 | 
						|
        key = prefix + key
 | 
						|
        key = key.replace(/[:\\\/"' {}.%]/g, "")
 | 
						|
        if (key.length >= 255) {
 | 
						|
            throw "Preferences: key length to big"
 | 
						|
        }
 | 
						|
        const cached = this.preferenceSources.get(key)
 | 
						|
        if (cached !== undefined) {
 | 
						|
            return cached
 | 
						|
        }
 | 
						|
        if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) {
 | 
						|
            this.UpdatePreferences()
 | 
						|
        }
 | 
						|
 | 
						|
        const pref = new UIEventSource<string>(
 | 
						|
            this.preferences.data[key] ?? defaultValue,
 | 
						|
            "osm-preference:" + key
 | 
						|
        )
 | 
						|
        pref.addCallback((v) => {
 | 
						|
            this.UploadPreference(key, v)
 | 
						|
        })
 | 
						|
 | 
						|
        this.preferenceSources.set(key, pref)
 | 
						|
        return pref
 | 
						|
    }
 | 
						|
 | 
						|
    public ClearPreferences() {
 | 
						|
        let isRunning = false
 | 
						|
        const self = this
 | 
						|
        this.preferences.addCallback((prefs) => {
 | 
						|
            console.log("Cleaning preferences...")
 | 
						|
            if (Object.keys(prefs).length == 0) {
 | 
						|
                return
 | 
						|
            }
 | 
						|
            if (isRunning) {
 | 
						|
                return
 | 
						|
            }
 | 
						|
            isRunning = true
 | 
						|
            const prefixes = ["mapcomplete-"]
 | 
						|
            for (const key in prefs) {
 | 
						|
                const matches = prefixes.some((prefix) => key.startsWith(prefix))
 | 
						|
                if (matches) {
 | 
						|
                    console.log("Clearing ", key)
 | 
						|
                    self.GetPreference(key, "", "").setData("")
 | 
						|
                }
 | 
						|
            }
 | 
						|
            isRunning = false
 | 
						|
            return
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
    private UpdatePreferences() {
 | 
						|
        const self = this
 | 
						|
        this.auth.xhr(
 | 
						|
            {
 | 
						|
                method: "GET",
 | 
						|
                path: "/api/0.6/user/preferences",
 | 
						|
            },
 | 
						|
            function (error, value: XMLDocument) {
 | 
						|
                if (error) {
 | 
						|
                    console.log("Could not load preferences", error)
 | 
						|
                    return
 | 
						|
                }
 | 
						|
                const prefs = value.getElementsByTagName("preference")
 | 
						|
                for (let i = 0; i < prefs.length; i++) {
 | 
						|
                    const pref = prefs[i]
 | 
						|
                    const k = pref.getAttribute("k")
 | 
						|
                    const v = pref.getAttribute("v")
 | 
						|
                    self.preferences.data[k] = v
 | 
						|
                }
 | 
						|
 | 
						|
                // We merge all the preferences: new keys are uploaded
 | 
						|
                // For differing values, the server overrides local changes
 | 
						|
                self.preferenceSources.forEach((preference, key) => {
 | 
						|
                    const osmValue = self.preferences.data[key]
 | 
						|
                    if (osmValue === undefined && preference.data !== undefined) {
 | 
						|
                        // OSM doesn't know this value yet
 | 
						|
                        self.UploadPreference(key, preference.data)
 | 
						|
                    } else {
 | 
						|
                        // OSM does have a value - set it
 | 
						|
                        preference.setData(osmValue)
 | 
						|
                    }
 | 
						|
                })
 | 
						|
 | 
						|
                self.preferences.ping()
 | 
						|
            }
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    private UploadPreference(k: string, v: string) {
 | 
						|
        if (!this.userDetails.data.loggedIn) {
 | 
						|
            console.debug(`Not saving preference ${k}: user not logged in`)
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        if (this.preferences.data[k] === v) {
 | 
						|
            return
 | 
						|
        }
 | 
						|
        console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15))
 | 
						|
 | 
						|
        if (v === undefined || v === "") {
 | 
						|
            this.auth.xhr(
 | 
						|
                {
 | 
						|
                    method: "DELETE",
 | 
						|
                    path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
 | 
						|
                    options: { header: { "Content-Type": "text/plain" } },
 | 
						|
                },
 | 
						|
                function (error) {
 | 
						|
                    if (error) {
 | 
						|
                        console.warn("Could not remove preference", error)
 | 
						|
                        return
 | 
						|
                    }
 | 
						|
                    console.debug("Preference ", k, "removed!")
 | 
						|
                }
 | 
						|
            )
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        this.auth.xhr(
 | 
						|
            {
 | 
						|
                method: "PUT",
 | 
						|
                path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
 | 
						|
                options: { header: { "Content-Type": "text/plain" } },
 | 
						|
                content: v,
 | 
						|
            },
 | 
						|
            function (error) {
 | 
						|
                if (error) {
 | 
						|
                    console.warn(`Could not set preference "${k}"'`, error)
 | 
						|
                    return
 | 
						|
                }
 | 
						|
                console.debug(`Preference ${k} written!`)
 | 
						|
            }
 | 
						|
        )
 | 
						|
    }
 | 
						|
}
 |