pxt-core
Version:
Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors
1,113 lines • 1.19 MB
JavaScript
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) {
data = pxt.Util.cleanData(data);
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>"}`;
pxt.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) {
data = pxt.Util.cleanData(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) {
data = pxt.Util.cleanData(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;
function trackPerformanceReport() {
if (pxt.perf.perfReportLogged)
return;
const data = pxt.perf.report();
if (data) {
const { durations, milestones } = data;
pxt.tickEvent("performance.milestones", milestones);
pxt.tickEvent("performance.durations", durations);
}
}
analytics.trackPerformanceReport = trackPerformanceReport;
})(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.
const INTERACTIVE_LOGIN_UNTIL = "interactive-login-until"; // hint whether to prompt user or try SSO first.
let authDisabled = false;
auth.DEFAULT_USER_PREFERENCES = () => ({
language: pxt.appTarget.appTheme.defaultLocale,
highContrast: false,
accessibleBlocks: false,
colorThemeIds: {},
reader: "",
skillmap: { mapProgress: {}, completedTags: {} },
email: false
});
let _client;
function client() {
if (authDisabled)
return undefined;
return _client;
}
auth.client = client;
const PREFERENCES_DEBOUNCE_MS = 1 * 1000;
const PREFERENCES_DEBOUNCE_MAX_MS = 10 * 1000;
let debouncePreferencesChangedTimeout = 0;
let debouncePreferencesChangedStarted = 0;
//
// Local storage of auth token.
//
// Last known auth token state. This is provided as a convenience for legacy methods that cannot be made async.
// Preference hasAuthTokenAsync() over taking a dependency on this cached value.
auth.cachedHasAuthToken = false;
async function setLocalStorageValueAsync(key, value) {
if (!!value)
return await pxt.storage.shared.setAsync(AUTH_CONTAINER, key, value);
else
return await pxt.storage.shared.delAsync(AUTH_CONTAINER, key);
}
async function getLocalStorageValueAsync(key) {
try {
return await pxt.storage.shared.getAsync(AUTH_CONTAINER, key);
}
catch (_a) {
return undefined;
}
}
async function getAuthTokenAsync() {
const token = await getLocalStorageValueAsync(CSRF_TOKEN_KEY);
auth.cachedHasAuthToken = !!token;
return token;
}
auth.getAuthTokenAsync = getAuthTokenAsync;
async function setAuthTokenAsync(token) {
auth.cachedHasAuthToken = !!token;
return await setLocalStorageValueAsync(CSRF_TOKEN_KEY, token);
}
async function hasAuthTokenAsync() {
return !!(await getAuthTokenAsync());
}
auth.hasAuthTokenAsync = hasAuthTokenAsync;
async function delAuthTokenAsync() {
auth.cachedHasAuthToken = false;
return await setLocalStorageValueAsync(CSRF_TOKEN_KEY, undefined);
}
async function getUserStateAsync() {
let userState;
try {
userState = await pxt.storage.shared.getAsync(AUTH_CONTAINER, AUTH_USER_STATE_KEY);
}
catch (_a) {
userState = {};
}
auth.cachedUserState = userState;
return userState;
}
auth.getUserStateAsync = getUserStateAsync;
async function setUserStateAsync(state) {
auth.cachedUserState = Object.assign({}, state);
return await pxt.storage.shared.setAsync(AUTH_CONTAINER, AUTH_USER_STATE_KEY, state);
}
async function delUserStateAsync() {
auth.cachedUserState = undefined;
return await pxt.storage.shared.delAsync(AUTH_CONTAINER, AUTH_USER_STATE_KEY);
}
async function getAuthHeadersAsync(authToken) {
var _a;
const headers = {};
authToken = authToken || (await getAuthTokenAsync());
if (authToken) {
headers["authorization"] = `mkcd ${authToken}`;
}
headers[X_PXT_TARGET] = (_a = pxt.appTarget) === null || _a === void 0 ? void 0 : _a.id;
return headers;
}
auth.getAuthHeadersAsync = getAuthHeadersAsync;
class AuthClient {
constructor() {
this.initialUserPreferences_ = undefined;
this.initialAuthCheck_ = undefined;
this.patchQueue = [];
// Set global instance.
_client = this;
}
async initAsync() {
// Initialize user state from local storage.
const state = await getUserStateAsync();
this.setUserProfileAsync(state === null || state === void 0 ? void 0 : state.profile);
this.setUserPreferencesAsync(state === null || state === void 0 ? void 0 : state.preferences);
}
/**
* 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) {
if (!hasIdentity() || !idpEnabled(idp)) {
return;
}
callbackState = callbackState !== null && callbackState !== void 0 ? callbackState : NilCallbackState;
// Clear local auth state so we can no longer make authenticated requests.
this.clearAuthStateAsync();
// 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
};
// Should the user be prompted to interactively login, or can we try to silently login?
const interactiveUntil = parseInt(await getLocalStorageValueAsync(INTERACTIVE_LOGIN_UNTIL));
const interactiveLogin = (interactiveUntil || 0) > Date.now();
// 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}`,
prompt: interactiveLogin ? "select_account" : "silent"
});
const apiResult = await this.apiAsync(loginUrl);
if (apiResult.success) {
loginState.authCodeVerifier = apiResult.resp.authCodeVerifier; // will be undefined unless configured for the target
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 {
try {
await this.onSignInFailed();
}
catch (_a) { }
}
}
/**
* Sign out the user and clear the auth token cookie.
*/
async logoutAsync(continuationHash) {
if (!hasIdentity()) {
return;
}
await AuthClient.staticLogoutAsync(continuationHash);
try {
await this.onStateCleared();
}
catch (_a) { }
try {
await this.onSignedOut();
}
catch (_b) { }
}
/**
* Sign out the user and clear the auth token cookie.
*/
static async staticLogoutAsync(continuationHash) {
if (!hasIdentity()) {
return;
}
pxt.tickEvent('auth.logout');
// Indicate that for the next minute, signin should be interactive.
// Use case: SSO signed in with the wrong account. User wants to sign in with a different account.
await setLocalStorageValueAsync(INTERACTIVE_LOGIN_UNTIL, (Date.now() + 60000).toString());
continuationHash = continuationHash ? continuationHash.startsWith('#') ? continuationHash : `#${continuationHash}` : "";
const clientRedirect = `${window.location.origin}${window.location.pathname}${window.location.search}${continuationHash}`;
// Tell backend to clear the http-only auth cookie.
let logoutUri = "";
try {
const uri = pxt.Util.stringifyQueryString('/api/auth/logout', {
redirect_uri: clientRedirect,
authcallback: '1'
});
const apiResult = await AuthClient.staticApiAsync(uri);
if (apiResult.success) {
logoutUri = apiResult.resp.logoutUrl;
}
}
catch (_a) {
// Ignore errors.
}
// Clear local auth state so we can no longer make authenticated requests.
await delAuthTokenAsync();
await delUserStateAsync();
if (pxt.BrowserUtils.hasWindow()) {
if (logoutUri) {
// Redirect to logout endpoint
window.location.href = logoutUri;
}
else {
// Redirect to home screen
window.location.href = clientRedirect;
location.reload();
}
}
}
async deleteProfileAsync() {
var _a;
// only if we're logged in
if (!await this.loggedInAsync()) {
return;
}
const state = await getUserStateAsync();
const userId = (_a = state === null || state === void 0 ? void 0 : state.profile) === null || _a === void 0 ? void 0 : _a.id;
const res = await this.apiAsync('/api/user', null, 'DELETE');
if (res.err) {
try {
await this.onApiError((res.err));
}
catch (_b) { }
}
else {
try {
// Clear local auth state so we can no longer make authenticated requests.
await this.clearAuthStateAsync();
try {
await this.onProfileDeleted(userId);
}
catch (_c) { }
}
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 = await getUserStateAsync();
return Object.assign({}, state.profile);
}
async userPreferencesAsync() {
//if (!await this.loggedInAsync()) { return undefined; } // allow even when not signed in.
const state = await getUserStateAsync();
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;
}
if (!(await hasAuthTokenAsync())) {
return undefined;
}
const state = await getUserStateAsync();
if ((_a = state === null || state === void 0 ? void 0 : state.profile) === null || _a === void 0 ? void 0 : _a.id) {
// If we already have a user profile, return it.
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() {
const profile = await this.authCheckAsync();
if (!profile)
return false;
return await this.hasUserIdAsync();
}
async updateUserProfileAsync(opts) {
if (!await this.loggedInAsync()) {
return false;
}
const state = await getUserStateAsync();
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
}
}
}
async hasUserIdAsync() {
var _a;
if (!hasIdentity()) {
return false;
}
if (!(await hasAuthTokenAsync())) {
return undefined;
}
const state = await getUserStateAsync();
return !!((_a = state === null || state === void 0 ? void 0 : state.profile) === null || _a === void 0 ? void 0 : _a.id);
}
async fetchUserAsync() {
var _a;
if (!hasIdentity()) {
return undefined;
}
if (!(await hasAuthTokenAsync())) {
return undefined;
}
const state = await getUserStateAsync();
// We already have a user, no need to get it again.
if ((_a = state === null || state === void 0 ? void 0 : 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 = await this.hasUserIdAsync();
await this.transformUserProfileAsync(profile);
const isLoggedIn = await this.hasUserIdAsync();
try {
await this.onUserProfileChanged();
}
catch (_a) { }
//pxt.data.invalidate(USER_PROFILE);
if (isLoggedIn && !wasLoggedIn) {
try {
await this.onSignedIn();
}
catch (_b) { }
//pxt.data.invalidate(LOGGED_IN);
}
else if (!isLoggedIn && wasLoggedIn) {
try {
await this.onSignedOut();
}
catch (_c) { }
//pxt.data.invalidate(LOGGED_IN);
}
}
async setUserPreferencesAsync(newPref) {
var _a;
const state = await getUserStateAsync();
const oldPref = (_a = state === null || state === void 0 ? void 0 : state.preferences) !== null && _a !== void 0 ? _a : auth.DEFAULT_USER_PREFERENCES();
const diff = ts.pxtc.jsonPatch.diff(oldPref, newPref);
// update
const finalPref = await this.transformUserPreferencesAsync(Object.assign(Object.assign({}, oldPref), newPref));
try {
await this.onUserPreferencesChanged(diff);
}
catch (_b) { }
return finalPref;
}
async fetchUserPreferencesAsync() {
// Wait for the initial auth
if (!await this.loggedInAsync()) {
return undefined;
}
const state = await getUserStateAsync();
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.
const prefs = this.setUserPreferencesAsync(result.resp);
// update our one-time promise for the initial load
return prefs;
}
}
return undefined;
}
/**
* Updates user profile state and writes it to local storage.
*/
async transformUserProfileAsync(profile) {
let state = await getUserStateAsync();
state = Object.assign(Object.assign({}, state), { profile: Object.assign({}, profile) });
await setUserStateAsync(state);
return state.profile;
}
/**
* Updates user preference state and writes it to local storage.
*/
async transformUserPreferencesAsync(preferences) {
let state = await getUserStateAsync();
state = Object.assign(Object.assign({}, state), { preferences: Object.assign({}, preferences) });
await setUserStateAsync(state);
return state.preferences;
}
/**
* Clear local auth state then call the onStateCleared callback.
*/
async clearAuthStateAsync() {
await delAuthTokenAsync();
await delUserStateAsync();
try {
await this.onStateCleared();
}
catch (_a) { }
}
/*protected*/ async apiAsync(url, data, method, authToken) {
return await AuthClient.staticApiAsync(url, data, method, authToken);
}
static async staticApiAsync(url, data, method, authToken) {
const headers = await getAuthHeadersAsync(authToken);
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 setAuthTokenAsync(authToken);
// Clear interactive login flag. Next auth request will try silent SSO first.
await setLocalStorageValueAsync(INTERACTIVE_LOGIN_UNTIL, undefined);
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}${params}${hash}`;
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(user) {
var _a;
return (_a = user === null || user === void 0 ? void 0 : user.idp) === null || _a === void 0 ? void 0 : _a.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 = {}));
var pxt;
(function (pxt) {
let LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["Debug"] = 0] = "Debug";
LogLevel[LogLevel["Info"] = 1] = "Info";
LogLevel[LogLevel["Log"] = 1] = "Log";
LogLevel[LogLevel["Warning"] = 2] = "Warning";
LogLevel[LogLevel["Error"] = 3] = "Error";
})(LogLevel = pxt.LogLevel || (pxt.LogLevel = {}));
class ConsoleLogger {
constructor() {
this.setLogLevel(LogLevel.Info);
}
setLogLevel(level) {
this.logLevel = level;
}
getLogLevel() {
return this.logLevel;
}
info(...args) {
if (!this.shouldLog(LogLevel.Info))
return;
if (console === null || console === void 0 ? void 0 : console.info) {
console.info.call(null, ...args);
}
}
log(...args) {
if (!this.shouldLog(LogLevel.Log))
return;
if (console === null || console === void 0 ? void 0 : console.log) {
console.log.call(null, ...args);
}
}
debug(...args) {
if (!this.shouldLog(LogLevel.Debug))
return;
if (console === null || console === void 0 ? void 0 : console.debug) {
console.debug.call(null, ...args);
}
}
error(...args) {
if (!this.shouldLog(LogLevel.Error))
return;
if (console === null || console === void 0 ? void 0 : console.error) {
console.error.call(null, ...args);
}
}
warn(...args) {
if (!this.shouldLog(LogLevel.Warning))
return;
if (console === null || console === void 0 ? void 0 : console.warn) {
console.warn.call(null, ...args);
}
}
shouldLog(level) {
return level >= this.logLevel;
}
}
pxt.ConsoleLogger = ConsoleLogger;
let logger = new ConsoleLogger();
function info(...args) {
logger.info(...args);
}
pxt.info = info;
function log(...args) {
logger.log(...args);
}
pxt.log = log;
function debug(...args) {
logger.debug(...args);
}
pxt.debug = debug;
function error(...args) {
logger.error(...args);
}
pxt.error = error;
function warn(...args) {
logger.warn(...args);
}
pxt.warn = warn;
function setLogger(impl) {
const level = logger === null || logger === void 0 ? void 0 : logger.getLogLevel();
logger = impl;
if (level !== undefined) {
logger.setLogLevel(level);
}
}
pxt.setLogger = setLogger;
function setLogLevel(level) {
logger.setLogLevel(level);
}
pxt.setLogLevel = setLogLevel;
})(pxt || (pxt = {}));
/// <reference path="./tickEvent.ts" />
/// <reference path="./apptarget.ts" />
/// <reference path="./logger.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,3})-(\w{2,4}$)/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)[