UNPKG

pxt-core

Version:

Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors

1,083 lines (1,082 loc) • 1.13 MB
var pxt; (function (pxt) { })(pxt || (pxt = {})); (function (pxt) { var analytics; (function (analytics) { const defaultProps = {}; const defaultMeasures = {}; let enabled = false; let ConsoleTickOptions; (function (ConsoleTickOptions) { ConsoleTickOptions[ConsoleTickOptions["Off"] = 0] = "Off"; ConsoleTickOptions[ConsoleTickOptions["Short"] = 1] = "Short"; ConsoleTickOptions[ConsoleTickOptions["Verbose"] = 2] = "Verbose"; })(ConsoleTickOptions = analytics.ConsoleTickOptions || (analytics.ConsoleTickOptions = {})); ; analytics.consoleTicks = ConsoleTickOptions.Off; function addDefaultProperties(props) { Object.keys(props).forEach(k => { if (typeof props[k] == "string") { defaultProps[k] = props[k]; } else { defaultMeasures[k] = props[k]; } }); } analytics.addDefaultProperties = addDefaultProperties; function enable(lang) { if (!pxt.aiTrackException || !pxt.aiTrackEvent || enabled) return; enabled = true; if (typeof lang != "string" || lang.length == 0) { lang = "en"; //Always have a default language. } addDefaultProperties({ lang: lang }); pxt.debug('setting up app insights'); const te = pxt.tickEvent; pxt.tickEvent = function (id, data, opts) { if (analytics.consoleTicks != ConsoleTickOptions.Off) { const prefix = analytics.consoleTicks == ConsoleTickOptions.Short ? "" : `${new Date().toLocaleTimeString(undefined, { hour12: false })} - Tick - `; const tickInfo = `${id} ${data ? JSON.stringify(data) : "<no data>"} ${opts ? JSON.stringify(opts) : "<no opts>"}`; console.log(prefix + tickInfo); } if (te) te(id, data, opts); if (opts === null || opts === void 0 ? void 0 : opts.interactiveConsent) pxt.setInteractiveConsent(true); if (!data) pxt.aiTrackEvent(id); else { const props = Object.assign({}, defaultProps) || {}; const measures = Object.assign({}, defaultMeasures) || {}; Object.keys(data).forEach(k => { if (typeof data[k] == "string") props[k] = data[k]; else if (typeof data[k] == "number") measures[k] = data[k]; else props[k] = JSON.stringify(data[k] || ''); }); pxt.aiTrackEvent(id, props, measures); } }; const rexp = pxt.reportException; pxt.reportException = function (err, data) { if (rexp) rexp(err, data); const props = { target: pxt.appTarget.id, version: pxt.appTarget.versions.target }; if (data) pxt.Util.jsonMergeFrom(props, data); pxt.aiTrackException(err, 'exception', props); }; const re = pxt.reportError; pxt.reportError = function (cat, msg, data) { if (re) re(cat, msg, data); try { throw msg; } catch (err) { const props = { target: pxt.appTarget.id, version: pxt.appTarget.versions.target, category: cat, message: msg }; if (data) pxt.Util.jsonMergeFrom(props, data); pxt.aiTrackException(err, 'error', props); } }; } analytics.enable = enable; })(analytics = pxt.analytics || (pxt.analytics = {})); })(pxt || (pxt = {})); var pxt; (function (pxt) { })(pxt || (pxt = {})); var pxt; (function (pxt) { var AudioContextManager; (function (AudioContextManager) { let _frequency = 0; let _context; // AudioContext let _vco; // OscillatorNode; let _gain; let _mute = false; //mute audio function context() { if (!_context) _context = freshContext(); return _context; } function freshContext() { window.AudioContext = window.AudioContext || window.webkitAudioContext; if (window.AudioContext) { try { // this call my crash. // SyntaxError: audio resources unavailable for AudioContext construction return new window.AudioContext(); } catch (e) { } } return undefined; } function mute(mute) { if (!_context) return; _mute = mute; stop(); if (mute && _vco) { _vco.disconnect(); _gain.disconnect(); _vco = undefined; _gain = undefined; } } AudioContextManager.mute = mute; function stop() { if (!_context) return; _gain.gain.setTargetAtTime(0, _context.currentTime, 0.015); _frequency = 0; } AudioContextManager.stop = stop; function frequency() { return _frequency; } AudioContextManager.frequency = frequency; function tone(frequency) { if (_mute) return; if (isNaN(frequency) || frequency < 0) return; _frequency = frequency; let ctx = context(); if (!ctx) return; try { if (!_vco) { _vco = ctx.createOscillator(); _vco.type = 'triangle'; _gain = ctx.createGain(); _gain.gain.value = 0; _gain.connect(ctx.destination); _vco.connect(_gain); _vco.start(0); } _vco.frequency.linearRampToValueAtTime(frequency, _context.currentTime); _gain.gain.setTargetAtTime(.2, _context.currentTime, 0.015); } catch (e) { _vco = undefined; return; } } AudioContextManager.tone = tone; })(AudioContextManager = pxt.AudioContextManager || (pxt.AudioContextManager = {})); })(pxt || (pxt = {})); var pxt; (function (pxt) { var auth; (function (auth) { const EMPTY_USERNAME = "??"; const AUTH_CONTAINER = "auth"; // local storage "namespace". const CSRF_TOKEN_KEY = "csrf-token"; // stored in local storage. const AUTH_LOGIN_STATE_KEY = "login-state"; // stored in local storage. const AUTH_USER_STATE_KEY = "user-state"; // stored in local storage. const X_PXT_TARGET = "x-pxt-target"; // header passed in auth rest calls. let authDisabled = false; auth.DEFAULT_USER_PREFERENCES = () => ({ highContrast: false, language: pxt.appTarget.appTheme.defaultLocale, reader: "", skillmap: { mapProgress: {}, completedTags: {} }, email: false }); let _client; function client() { return _client; } auth.client = client; const PREFERENCES_DEBOUNCE_MS = 1 * 1000; const PREFERENCES_DEBOUNCE_MAX_MS = 10 * 1000; let debouncePreferencesChangedTimeout = 0; let debouncePreferencesChangedStarted = 0; class AuthClient { constructor() { this.initialUserPreferences_ = undefined; this.initialAuthCheck_ = undefined; this.patchQueue = []; // Set global instance. _client = this; } async initAsync() { // Load state from local storage try { this.state$ = await pxt.storage.shared.getAsync(AUTH_CONTAINER, AUTH_USER_STATE_KEY); } catch (_a) { } if (!this.state$) { this.state$ = {}; } this.setUserProfileAsync(this.state$.profile); this.setUserPreferencesAsync(this.state$.preferences); } async authTokenAsync() { return await pxt.storage.shared.getAsync(AUTH_CONTAINER, CSRF_TOKEN_KEY); } /** * Starts the process of authenticating the user against the given identity * provider. Upon success the backend will write an http-only session cookie * to the response, containing the authorization token. This cookie is not * accessible in code, but will be included in all subsequent http requests. * @param idp The id of the identity provider. * @param persistent Whether or not to remember this login across sessions. * @param callbackState The URL hash and params to return to after the auth * flow completes. */ async loginAsync(idp, persistent, callbackState = undefined) { var _a; if (!hasIdentity() || !idpEnabled(idp)) { return; } callbackState = callbackState !== null && callbackState !== void 0 ? callbackState : NilCallbackState; const state = this.getState(); // See if we have a valid access token already. if (!state.profile) { await this.authCheckAsync(); } const currIdp = (_a = state.profile) === null || _a === void 0 ? void 0 : _a.idp; // Check if we're already signed into this identity provider. if ((currIdp === null || currIdp === void 0 ? void 0 : currIdp.provider) === idp) { pxt.debug(`loginAsync: Already signed into ${idp}.`); return; } this.clearState(); // Store some continuation state in local storage so we can return to what // the user was doing before signing in. const genId = () => (Math.PI * Math.random()).toString(36).slice(2); const loginState = { key: genId(), callbackState, callbackPathname: window.location.pathname, idp, persistent }; // Redirect to the login endpoint. const loginUrl = pxt.Util.stringifyQueryString('/api/auth/login', { response_type: "token", provider: idp, persistent, redirect_uri: `${window.location.origin}${window.location.pathname}?authcallback=1&state=${loginState.key}` }); const apiResult = await this.apiAsync(loginUrl); if (apiResult.success) { loginState.authCodeVerifier = apiResult.resp.authCodeVerifier; await pxt.storage.shared.setAsync(AUTH_CONTAINER, AUTH_LOGIN_STATE_KEY, loginState); pxt.tickEvent('auth.login.start', { 'provider': idp }); window.location.href = apiResult.resp.loginUrl; } else { await this.onSignInFailed(); } } /** * Sign out the user and clear the auth token cookie. */ async logoutAsync(continuationHash) { if (!hasIdentity()) { return; } this.clearState(); return await AuthClient.staticLogoutAsync(continuationHash); } /** * Sign out the user and clear the auth token cookie. */ static async staticLogoutAsync(continuationHash) { if (!hasIdentity()) { return; } pxt.tickEvent('auth.logout'); // backend will clear the cookie token and pass back the provider logout endpoint. await AuthClient.staticApiAsync('/api/auth/logout'); // Clear csrf token so we can no longer make authenticated requests. await pxt.storage.shared.delAsync(AUTH_CONTAINER, CSRF_TOKEN_KEY); // Update state and UI to reflect logged out state. const hash = continuationHash ? continuationHash.startsWith('#') ? continuationHash : `#${continuationHash}` : ""; // Redirect to home screen, or skillmap home screen if (pxt.BrowserUtils.hasWindow()) { window.location.href = `${window.location.origin}${window.location.pathname}${hash}`; location.reload(); } } async deleteProfileAsync() { var _a; // only if we're logged in if (!await this.loggedInAsync()) { return; } const userId = (_a = this.getState().profile) === null || _a === void 0 ? void 0 : _a.id; const res = await this.apiAsync('/api/user', null, 'DELETE'); if (res.err) { await this.onApiError((res.err)); } else { try { // Clear csrf token so we can no longer make authenticated requests. await pxt.storage.shared.delAsync(AUTH_CONTAINER, CSRF_TOKEN_KEY); try { await this.onProfileDeleted(userId); } catch (_b) { pxt.tickEvent('auth.profile.cloudToLocalFailed'); } // Update state and UI to reflect logged out state. this.clearState(); } finally { pxt.tickEvent('auth.profile.deleted'); } } } async initialUserPreferencesAsync() { // only if we're logged in if (!await this.loggedInAsync()) { return undefined; } if (!this.initialUserPreferences_) { this.initialUserPreferences_ = this.fetchUserPreferencesAsync(); } return this.initialUserPreferences_; } async userProfileAsync() { if (!await this.loggedInAsync()) { return undefined; } const state = this.getState(); return Object.assign({}, state.profile); } async userPreferencesAsync() { //if (!await this.loggedInAsync()) { return undefined; } // allow even when not signed in. const state = this.getState(); return Object.assign({}, state.preferences); } /** * Checks to see if we're already logged in by trying to fetch user info from * the backend. If we have a valid auth token cookie, it will succeed. */ async authCheckAsync() { var _a; if (!hasIdentity()) { return undefined; } // Fail fast if we don't have csrf token. if (!(await pxt.storage.shared.getAsync(AUTH_CONTAINER, CSRF_TOKEN_KEY))) { return undefined; } const state = this.getState(); if ((_a = state.profile) === null || _a === void 0 ? void 0 : _a.id) { if (!this.initialAuthCheck_) { this.initialAuthCheck_ = Promise.resolve(state.profile); } } else if (!this.initialAuthCheck_) { // Optimistically try to fetch user profile. It will succeed if we have a valid // session cookie. Upon success, virtual api state will be updated, and the UI // will update accordingly. this.initialAuthCheck_ = this.fetchUserAsync(); } return this.initialAuthCheck_; } async loggedInAsync() { await this.authCheckAsync(); return this.hasUserId(); } async updateUserProfileAsync(opts) { if (!await this.loggedInAsync()) { return false; } const state = this.getState(); const result = await this.apiAsync('/api/user/profile', { id: state.profile.id, username: opts.username, avatarUrl: opts.avatarUrl }); if (result.success) { // Set user profile from returned value await this.setUserProfileAsync(result.resp); } return result.success; } async patchUserPreferencesAsync(patchOps, opts = {}) { const defaultSuccessAsync = async () => ({ success: true, res: await this.userPreferencesAsync() }); patchOps = Array.isArray(patchOps) ? patchOps : [patchOps]; patchOps = patchOps.filter(op => !!op); if (!patchOps.length) { return await defaultSuccessAsync(); } const patchDiff = (pSrc, ops, filter) => { // Apply patches to pDst and return the diff as a set of new patch ops. const pDst = pxt.U.deepCopy(pSrc); ts.pxtc.jsonPatch.patchInPlace(pDst, ops); let diff = ts.pxtc.jsonPatch.diff(pSrc, pDst); // Run caller-provided filter if (diff.length && filter) { diff = diff.filter(filter); } return diff; }; // Process incoming patch operations to produce a more fine-grained set of diffs. Incoming patches may be overly destructive // Apply the patch in isolation and get the diff from original const curPref = await this.userPreferencesAsync(); const diff = patchDiff(curPref, patchOps, opts.filter); if (!diff.length) { return await defaultSuccessAsync(); } // Apply the new diff to the current state ts.pxtc.jsonPatch.patchInPlace(curPref, diff); await this.setUserPreferencesAsync(curPref); // If the user is not logged in, non-persistent local state is all we'll use (no sync to cloud) if (!await this.loggedInAsync()) { return await defaultSuccessAsync(); } // If the user is logged in, sync to cloud, but debounce the api call as this can be called frequently from skillmaps // Queue the patch for sync with backend this.patchQueue.push({ ops: patchOps, filter: opts.filter }); clearTimeout(debouncePreferencesChangedTimeout); const syncPrefs = async () => { debouncePreferencesChangedStarted = 0; if (!this.patchQueue.length) { return await defaultSuccessAsync(); } // Fetch latest prefs from remote const getResult = await this.apiAsync('/api/user/preferences'); if (!getResult.success) { pxt.reportError("identity", "failed to fetch preferences for patch", getResult); return { success: false, res: undefined }; } // Apply queued patches to the remote state in isolation and develop a final diff to send to the backend const remotePrefs = pxt.U.deepCopy(getResult.resp) || auth.DEFAULT_USER_PREFERENCES(); const patchQueue = this.patchQueue; this.patchQueue = []; // Reset the queue patchQueue.forEach(patch => { const diff = patchDiff(remotePrefs, patch.ops, patch.filter); ts.pxtc.jsonPatch.patchInPlace(remotePrefs, diff); }); // Diff the original and patched remote states to get a final set of patch operations const finalOps = pxtc.jsonPatch.diff(getResult.resp, remotePrefs); const patchResult = await this.apiAsync('/api/user/preferences', finalOps, 'PATCH'); if (patchResult.success) { // Set user profile from returned value so we stay in sync this.setUserPreferencesAsync(patchResult.resp); } else { pxt.reportError("identity", "failed to patch preferences", patchResult); } return { success: patchResult.success, res: patchResult.resp }; }; if (opts.immediate) { return await syncPrefs(); } else { if (!debouncePreferencesChangedStarted) { debouncePreferencesChangedStarted = pxt.U.now(); } if (PREFERENCES_DEBOUNCE_MAX_MS < pxt.U.now() - debouncePreferencesChangedStarted) { return await syncPrefs(); } else { debouncePreferencesChangedTimeout = setTimeout(syncPrefs, PREFERENCES_DEBOUNCE_MS); return { success: false, res: undefined }; // This needs to be implemented correctly to return a promise with the debouncer } } } /*protected*/ hasUserId() { var _a; if (!hasIdentity()) { return false; } const state = this.getState(); return !!((_a = state.profile) === null || _a === void 0 ? void 0 : _a.id); } async fetchUserAsync() { var _a; if (!hasIdentity()) { return undefined; } const state = this.getState(); // We already have a user, no need to get it again. if ((_a = state.profile) === null || _a === void 0 ? void 0 : _a.id) { return state.profile; } const result = await this.apiAsync('/api/user/profile'); if (result.success) { const profile = result.resp; await this.setUserProfileAsync(profile); return profile; } return undefined; } async setUserProfileAsync(profile) { const wasLoggedIn = this.hasUserId(); this.transformUserProfile(profile); const isLoggedIn = this.hasUserId(); this.onUserProfileChanged(); //pxt.data.invalidate(USER_PROFILE); if (isLoggedIn && !wasLoggedIn) { await this.onSignedIn(); //pxt.data.invalidate(LOGGED_IN); } else if (!isLoggedIn && wasLoggedIn) { await this.onSignedOut(); //pxt.data.invalidate(LOGGED_IN); } } async setUserPreferencesAsync(newPref) { var _a; const oldPref = (_a = this.getState().preferences) !== null && _a !== void 0 ? _a : auth.DEFAULT_USER_PREFERENCES(); const diff = ts.pxtc.jsonPatch.diff(oldPref, newPref); // update this.transformUserPreferences(Object.assign(Object.assign({}, oldPref), newPref)); await this.onUserPreferencesChanged(diff); } async fetchUserPreferencesAsync() { // Wait for the initial auth if (!await this.loggedInAsync()) { return undefined; } const state = this.getState(); const result = await this.apiAsync('/api/user/preferences'); if (result.success) { // Set user profile from returned value if (result.resp) { // Note the cloud should send partial information back if it is missing // a field. So e.g. if the language has never been set in the cloud, it won't // overwrite the local state. this.setUserPreferencesAsync(result.resp); // update our one-time promise for the initial load return state.preferences; } } return undefined; } /** * Updates user profile state and writes it to local storage. * Direct access to state$ allowed. */ transformUserProfile(profile) { this.state$ = Object.assign(Object.assign({}, this.state$), { profile: Object.assign({}, profile) }); this.saveState(); } /** * Updates user preference state and writes it to local storage. * Direct access to state$ allowed. */ transformUserPreferences(preferences) { this.state$ = Object.assign(Object.assign({}, this.state$), { preferences: Object.assign({}, preferences) }); this.saveState(); } /** * Read-only access to current state. * Direct access to state$ allowed. */ /*private*/ getState() { return this.state$; } ; /** * Write auth state to local storage. * Direct access to state$ allowed. */ saveState() { pxt.storage.shared.setAsync(AUTH_CONTAINER, AUTH_USER_STATE_KEY, this.state$).then(() => { }); } /** * Clear all auth state. * Direct access to state$ allowed. */ clearState() { this.state$ = {}; pxt.storage.shared.delAsync(AUTH_CONTAINER, AUTH_USER_STATE_KEY) .then(() => this.onStateCleared()); } /*protected*/ async apiAsync(url, data, method, authToken) { return await AuthClient.staticApiAsync(url, data, method, authToken); } static async staticApiAsync(url, data, method, authToken) { var _a; const headers = {}; authToken = authToken || (await pxt.storage.shared.getAsync(AUTH_CONTAINER, CSRF_TOKEN_KEY)); if (authToken) { headers["authorization"] = `mkcd ${authToken}`; } headers[X_PXT_TARGET] = (_a = pxt.appTarget) === null || _a === void 0 ? void 0 : _a.id; url = pxt.BrowserUtils.isLocalHostDev() ? `${pxt.cloud.DEV_BACKEND}${url}` : url; return pxt.Util.requestAsync({ url, headers, data, method: method ? method : data ? "POST" : "GET", withCredentials: true, // Include cookies and authorization header in request, subject to CORS policy. }).then(r => { return { statusCode: r.statusCode, resp: r.json, success: Math.floor(r.statusCode / 100) === 2, err: null }; }).catch(async (e) => { if (!/logout/.test(url) && e.statusCode == 401) { // 401/Unauthorized. logout now. await AuthClient.staticLogoutAsync(); } return { statusCode: e.statusCode, err: e, resp: null, success: false }; }); } } auth.AuthClient = AuthClient; const NilCallbackState = { hash: '', params: {} }; async function loginCallbackAsync(qs) { let loginState; let callbackState = Object.assign({}, NilCallbackState); do { // Read and remove auth state from local storage loginState = await pxt.storage.shared.getAsync(AUTH_CONTAINER, AUTH_LOGIN_STATE_KEY); if (!loginState) { pxt.debug("Auth state not found in storge."); return; } await pxt.storage.shared.delAsync(AUTH_CONTAINER, AUTH_LOGIN_STATE_KEY); const stateKey = qs['state']; if (!stateKey || loginState.key !== stateKey) { pxt.debug("Failed to get auth state for key"); return; } callbackState = Object.assign(Object.assign({}, NilCallbackState), loginState.callbackState); const error = qs['error']; if (error) { // Possible values for 'error': // 'invalid_request' -- Something is wrong with the request itself. // 'access_denied' -- The identity provider denied the request, or user canceled it. const error_description = qs['error_description']; pxt.tickEvent('auth.login.error', { 'error': error, 'provider': loginState.idp }); pxt.log(`Auth failed: ${error}:${error_description}`); // TODO: Is it correct to clear continuation hash? callbackState = Object.assign({}, NilCallbackState); // TODO: Show a message to the user (via rewritten continuation path)? break; } const authToken = qs['token']; if (!authToken) { pxt.debug("Missing authToken in auth callback."); break; } // If this auth request was assigned an auth code, claim it now. This will set // the required auth cookie in this domain (for cross-domain authentication). if (loginState.authCodeVerifier) { const otacCheckUrl = pxt.Util.stringifyQueryString('/api/otac/check', { persistent: loginState.persistent, }); await AuthClient.staticApiAsync(otacCheckUrl, null, null, loginState.authCodeVerifier); } // Store csrf token in local storage. It is ok to do this even when // "Remember me" wasn't selected because this token is not usable // without its cookie-based counterpart. When "Remember me" is false, // the cookie is not persisted. await pxt.storage.shared.setAsync(AUTH_CONTAINER, CSRF_TOKEN_KEY, authToken); pxt.tickEvent('auth.login.success', { 'provider': loginState.idp }); } while (false); // Clear url parameters and redirect to the callback location. const hash = callbackState.hash.startsWith('#') ? callbackState.hash : `#${callbackState.hash}`; const params = pxt.Util.stringifyQueryString('', callbackState.params); const pathname = loginState.callbackPathname.startsWith('/') ? loginState.callbackPathname : `/${loginState.callbackPathname}`; const redirect = `${pathname}${hash}${params}`; window.location.href = redirect; } auth.loginCallbackAsync = loginCallbackAsync; function identityProviders() { var _a, _b; return Object.keys(((_b = (_a = pxt.appTarget) === null || _a === void 0 ? void 0 : _a.cloud) === null || _b === void 0 ? void 0 : _b.cloudProviders) || {}) .map(id => pxt.appTarget.cloud.cloudProviders[id]) .filter(prov => prov.identity) .sort((a, b) => a.order - b.order); } auth.identityProviders = identityProviders; function identityProvider(id) { return identityProviders().filter(prov => prov.id === id).shift(); } auth.identityProvider = identityProvider; function hasIdentity() { return !authDisabled && !pxt.BrowserUtils.isPxtElectron() && identityProviders().length > 0; } auth.hasIdentity = hasIdentity; function idpEnabled(idp) { return identityProviders().filter(prov => prov.id === idp).length > 0; } function enableAuth(enabled = true) { authDisabled = !enabled; } auth.enableAuth = enableAuth; function userName(user) { var _a, _b, _c, _d; return (_d = (_b = (_a = user === null || user === void 0 ? void 0 : user.idp) === null || _a === void 0 ? void 0 : _a.displayName) !== null && _b !== void 0 ? _b : (_c = user === null || user === void 0 ? void 0 : user.idp) === null || _c === void 0 ? void 0 : _c.username) !== null && _d !== void 0 ? _d : EMPTY_USERNAME; } auth.userName = userName; function identityProviderId() { var _a, _b, _c, _d; return (_d = (_c = (_b = (_a = client()) === null || _a === void 0 ? void 0 : _a.getState()) === null || _b === void 0 ? void 0 : _b.profile) === null || _c === void 0 ? void 0 : _c.idp) === null || _d === void 0 ? void 0 : _d.provider; } auth.identityProviderId = identityProviderId; function firstName(user) { const userName = pxt.auth.userName(user); return (userName === null || userName === void 0 ? void 0 : userName.split(" ").shift()) || userName; } auth.firstName = firstName; function userInitials(user) { const username = pxt.auth.userName(user); return ts.pxtc.Util.initials(username); } auth.userInitials = userInitials; function generateUserProfilePicDataUrl(profile) { var _a, _b; if ((_b = (_a = profile === null || profile === void 0 ? void 0 : profile.idp) === null || _a === void 0 ? void 0 : _a.picture) === null || _b === void 0 ? void 0 : _b.encoded) { const url = window.URL || window.webkitURL; try { // Decode the base64 image to a data URL. const decoded = pxt.Util.stringToUint8Array(atob(profile.idp.picture.encoded)); const blob = new Blob([decoded], { type: profile.idp.picture.mimeType }); profile.idp.picture.dataUrl = url.createObjectURL(blob); } catch (_c) { } } } auth.generateUserProfilePicDataUrl = generateUserProfilePicDataUrl; /** * Checks only the ID and sourceURL */ function badgeEquals(badgeA, badgeB) { return badgeA.id === badgeB.id && badgeA.sourceURL === badgeB.sourceURL; } auth.badgeEquals = badgeEquals; function hasBadge(preferences, badge) { return preferences.badges.some(toCheck => badgeEquals(toCheck, badge)); } auth.hasBadge = hasBadge; })(auth = pxt.auth || (pxt.auth = {})); })(pxt || (pxt = {})); // Needs to be in its own file to avoid a circular dependency: util.ts -> main.ts -> util.ts var pxt; (function (pxt) { /** * Track an event. */ pxt.tickEvent = function (id) { }; })(pxt || (pxt = {})); /// <reference path="./tickEvent.ts" /> /// <reference path="./apptarget.ts" /> var ts; (function (ts) { var pxtc; (function (pxtc) { pxtc.__dummy = 42; })(pxtc = ts.pxtc || (ts.pxtc = {})); })(ts || (ts = {})); var pxtc = ts.pxtc; (function (ts) { var pxtc; (function (pxtc) { var Util; (function (Util) { function assert(cond, msg = "Assertion failed") { if (!cond) { debugger; throw new Error(msg); } } Util.assert = assert; function flatClone(obj) { if (obj == null) return null; let r = {}; Object.keys(obj).forEach((k) => { r[k] = obj[k]; }); return r; } Util.flatClone = flatClone; function clone(v) { if (v == null) return null; return JSON.parse(JSON.stringify(v)); } Util.clone = clone; function htmlEscape(_input) { if (!_input) return _input; // null, undefined, empty string test return _input.replace(/([^\w .!?\-$])/g, c => "&#" + c.charCodeAt(0) + ";"); } Util.htmlEscape = htmlEscape; function htmlUnescape(_input) { if (!_input) return _input; // null, undefined, empty string test return _input.replace(/(&#\d+;)/g, c => String.fromCharCode(Number(c.substr(2, c.length - 3)))); } Util.htmlUnescape = htmlUnescape; function jsStringQuote(s) { return s.replace(/[^\w .!?\-$]/g, (c) => { let h = c.charCodeAt(0).toString(16); return "\\u" + "0000".substr(0, 4 - h.length) + h; }); } Util.jsStringQuote = jsStringQuote; function jsStringLiteral(s) { return "\"" + jsStringQuote(s) + "\""; } Util.jsStringLiteral = jsStringLiteral; function initials(username) { if (/^\w+@/.test(username)) { // Looks like an email address. Return first two characters. const initials = username.match(/^\w\w/); return initials.shift().toUpperCase(); } else { // Parse the user name for user initials const initials = username.match(/\b\w/g) || []; return ((initials.shift() || '') + (initials.pop() || '')).toUpperCase(); } } Util.initials = initials; // Localization functions. Please port any modifications over to pxtsim/localization.ts let _localizeLang = "en"; let _localizeStrings = {}; let _translationsCache = {}; //let _didSetlocalizations = false; //let _didReportLocalizationsNotSet = false; let localizeLive = false; function enableLiveLocalizationUpdates() { localizeLive = true; } Util.enableLiveLocalizationUpdates = enableLiveLocalizationUpdates; function liveLocalizationEnabled() { return localizeLive; } Util.liveLocalizationEnabled = liveLocalizationEnabled; /** * Returns the current user language, prepended by "live-" if in live mode */ function localeInfo() { return `${localizeLive ? "live-" : ""}${userLanguage()}`; } Util.localeInfo = localeInfo; /** * Returns current user language iSO-code. Default is `en`. */ function userLanguage() { return _localizeLang; } Util.userLanguage = userLanguage; // This function returns normalized language code // For example: zh-CN this returns ["zh-CN", "zh", "zh-cn"] // First two are valid crowdin\makecode locale code, // Last all lowercase one is just for the backup when reading user defined extensions & tutorials. function normalizeLanguageCode(code) { const langParts = /^(\w{2})-(\w{2}$)/i.exec(code); if (langParts && langParts[1] && langParts[2]) { return [`${langParts[1].toLowerCase()}-${langParts[2].toUpperCase()}`, langParts[1].toLowerCase(), `${langParts[1].toLowerCase()}-${langParts[2].toLowerCase()}`]; } else { return [(code || "en").toLowerCase()]; } } Util.normalizeLanguageCode = normalizeLanguageCode; function setUserLanguage(localizeLang) { _localizeLang = normalizeLanguageCode(localizeLang)[0]; } Util.setUserLanguage = setUserLanguage; function isUserLanguageRtl() { return /^ar|dv|fa|ha|he|ks|ku|ps|ur|yi/i.test(_localizeLang); } Util.isUserLanguageRtl = isUserLanguageRtl; Util.TRANSLATION_LOCALE = "pxt"; function isTranslationMode() { return userLanguage() == Util.TRANSLATION_LOCALE; } Util.isTranslationMode = isTranslationMode; function _localize(s) { // Needs to be test in localhost / CLI /*if (!_didSetlocalizations && !_didReportLocalizationsNotSet) { _didReportLocalizationsNotSet = true; pxt.tickEvent("locale.localizationsnotset"); // pxt.reportError can't be used here because of order of file imports // Just use console.error instead, and use an Error so stacktrace is reported console.error(new Error("Attempted to translate a string before localizations were set")); }*/ return _localizeStrings[s] || s; } Util._localize = _localize; function getLocalizedStrings() { return _localizeStrings; } Util.getLocalizedStrings = getLocalizedStrings; function setLocalizedStrings(strs) { //_didSetlocalizations = true; _localizeStrings = strs; } Util.setLocalizedStrings = setLocalizedStrings; function translationsCache() { return _translationsCache; } Util.translationsCache = translationsCache; function fmt_va(f, args) { if (args.length == 0) return f; return f.replace(/\{([0-9]+)(\:[^\}]+)?\}/g, function (s, n, spec) { let v = args[parseInt(n)]; let r = ""; let fmtMatch = /^:f(\d*)\.(\d+)/.exec(spec); if (fmtMatch) { let precision = parseInt(fmtMatch[2]); let len = parseInt(fmtMatch[1]) || 0; let fillChar = /^0/.test(fmtMatch[1]) ? "0" : " "; let num = v.toFixed(precision); if (len > 0 && precision > 0) len += precision + 1; if (len > 0) { while (num.length < len) { num = fillChar + num; } } r = num; } else if (spec == ":x") { r = "0x" + v.toString(16); } else if (v === undefined) r = "(undef)"; else if (v === null) r = "(null)"; else if (v.toString) r = v.toString(); else r = v + ""; if (spec == ":a") { if (/^\s*[euioah]/.test(r.toLowerCase())) r = "an " + r; else if (/^\s*[bcdfgjklmnpqrstvwxz]/.test(r.toLowerCase())) r = "a " + r; } else if (spec == ":s") { if (v == 1) r = ""; else r = "s"; } else if (spec == ":q") { r = Util.htmlEscape(r); } else if (spec == ":jq") { r = Util.jsStringQuote(r); } else if (spec == ":uri") { r = encodeURIComponent(r).replace(/'/g, "%27").replace(/"/g, "%22"); } else if (spec == ":url") { r = encodeURI(r).replace(/'/g, "%27").replace(/"/g, "%22"); } else if (spec == ":%") { r = (v * 100).toFixed(1).toString() + '%'; } return r; }); } Util.fmt_va = fmt_va; function fmt(f, ...args) { return fmt_va(f, args); } Util.fmt = fmt; const locStats = {}; function dumpLocStats() { const r = {}; Object.keys(locStats).sort((a, b) => locStats[b] - locStats[a]) .forEach(k => r[k] = k); console.log('prioritized list of strings:'); console.log(JSON.stringify(r, null, 2)); } Util.dumpLocStats = dumpLocStats; let sForPlural = true; function lf_va(format, args) { if (!format) return format; locStats[format] = (locStats[format] || 0) + 1; let lfmt = Util._localize(format); if (!sForPlural && lfmt != format && /\d:s\}/.test(lfmt)) { lfmt = lfmt.replace(/\{\d+:s\}/g, ""); } lfmt = lfmt.replace(/^\{(id|loc):[^\}]+\}/g, ''); return fmt_va(lfmt, args); } Util.lf_va = lf_va; function lf(format, ...args) { return lf_va(format, args); // @ignorelf@ } Util.lf = lf; /** * Similar to lf but the string do not get extracted into the loc file. */ function rlf(format, ...args) { return lf_va(format, args); // @ignorelf@ } Util.rlf = rlf; function lookup(m, key) { if (m.hasOwnProperty(key)) return m[key]; return null; } Util.lookup = lookup; function isoTime(time) { let d = new Date(time * 1000); return Util.fmt("{0}-{1:f02.0}-{2:f02.0} {3:f02.0}:{4:f02.0}:{5:f02.0}", d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()); } Util.isoTime = isoTime; function userError(msg) { let e = new Error(msg); e.isUserError = true; throw e; } Util.userError = userError; // small deep equals for primitives, objects, arrays. returns error message function deq(a, b) { if (a === b) return null; if (!a || !b) return "Null value"; if (typeof a == 'object' && typeof b == 'object') { if (Array.isArray(a)) { if (!Array.isArray(b)) { return "Expected array"; } if (a.length != b.length) { return "Expected array of length " + a.length + ", got " + b.length; } for (let i = 0; i < a.length; i++) { if (deq(a[i], b[i]) != null) { return "Expected array value " + a[i] + " got " + b[i]; } } return null; } let ak = Object.keys(a); let bk = Object.keys(a); if (ak.length != bk.length) { return "Expected " + ak.length + " keys, got " + bk.length; } for (let i = 0; i < ak.length; i++) {