diff --git a/cloud.js b/cloud.js index 28b2a6c5..dfb359b8 100644 --- a/cloud.js +++ b/cloud.js @@ -1,658 +1,504 @@ -/* - - cloud.js - - a backend API for SNAP! - - written by Jens Mönig - - Copyright (C) 2015 by Jens Mönig - - This file is part of Snap!. - - Snap! is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of - the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - -// Global settings ///////////////////////////////////////////////////// - -/*global modules, IDE_Morph, SnapSerializer, hex_sha512, alert, nop, -localize*/ - -modules.cloud = '2015-December-15'; - -// Global stuff - -var Cloud; -var SnapCloud = new Cloud( - 'https://snap.apps.miosoft.com/SnapCloud' -); - -// Cloud ///////////////////////////////////////////////////////////// - -function Cloud(url) { - this.username = null; - this.password = null; // hex_sha512 hashed - this.url = url; - this.session = null; - this.limo = null; - this.route = null; - this.api = {}; -} - -Cloud.prototype.clear = function () { - this.username = null; - this.password = null; - this.session = null; - this.limo = null; - this.route = null; - this.api = {}; -}; - -Cloud.prototype.hasProtocol = function () { - return this.url.toLowerCase().indexOf('http') === 0; -}; - -Cloud.prototype.setRoute = function (username) { - var routes = 20, - userNum = 0, - i; - - for (i = 0; i < username.length; i += 1) { - userNum += username.charCodeAt(i); - } - userNum = userNum % routes + 1; - this.route = '.sc1m' + - (userNum < 10 ? '0' : '') + - userNum; -}; - -// Cloud: Snap! API - -Cloud.prototype.signup = function ( - username, - email, - callBack, - errorCall -) { - // both callBack and errorCall are two-argument functions - var request = new XMLHttpRequest(), - myself = this; - try { - request.open( - "GET", - (this.hasProtocol() ? '' : 'http://') - + this.url + 'SignUp' - + '?Username=' - + encodeURIComponent(username) - + '&Email=' - + encodeURIComponent(email), - true - ); - request.setRequestHeader( - "Content-Type", - "application/x-www-form-urlencoded" - ); - request.withCredentials = true; - request.onreadystatechange = function () { - if (request.readyState === 4) { - if (request.responseText) { - if (request.responseText.indexOf('ERROR') === 0) { - errorCall.call( - this, - request.responseText, - 'Signup' - ); - } else { - callBack.call( - null, - request.responseText, - 'Signup' - ); - } - } else { - errorCall.call( - null, - myself.url + 'SignUp', - localize('could not connect to:') - ); - } - } - }; - request.send(null); - } catch (err) { - errorCall.call(this, err.toString(), 'Snap!Cloud'); - } -}; - -Cloud.prototype.getPublicProject = function ( - id, - callBack, - errorCall -) { - // id is Username=username&projectName=projectname, - // where the values are url-component encoded - // callBack is a single argument function, errorCall take two args - var request = new XMLHttpRequest(), - myself = this; - try { - request.open( - "GET", - (this.hasProtocol() ? '' : 'http://') - + this.url + 'RawPublic' - + '?' - + id, - true - ); - request.setRequestHeader( - "Content-Type", - "application/x-www-form-urlencoded" - ); - request.withCredentials = true; - request.onreadystatechange = function () { - if (request.readyState === 4) { - if (request.responseText) { - if (request.responseText.indexOf('ERROR') === 0) { - errorCall.call( - this, - request.responseText - ); - } else { - callBack.call( - null, - request.responseText - ); - } - } else { - errorCall.call( - null, - myself.url + 'Public', - localize('could not connect to:') - ); - } - } - }; - request.send(null); - } catch (err) { - errorCall.call(this, err.toString(), 'Snap!Cloud'); - } -}; - -Cloud.prototype.resetPassword = function ( - username, - callBack, - errorCall -) { - // both callBack and errorCall are two-argument functions - var request = new XMLHttpRequest(), - myself = this; - try { - request.open( - "GET", - (this.hasProtocol() ? '' : 'http://') - + this.url + 'ResetPW' - + '?Username=' - + encodeURIComponent(username), - true - ); - request.setRequestHeader( - "Content-Type", - "application/x-www-form-urlencoded" - ); - request.withCredentials = true; - request.onreadystatechange = function () { - if (request.readyState === 4) { - if (request.responseText) { - if (request.responseText.indexOf('ERROR') === 0) { - errorCall.call( - this, - request.responseText, - 'Reset Password' - ); - } else { - callBack.call( - null, - request.responseText, - 'Reset Password' - ); - } - } else { - errorCall.call( - null, - myself.url + 'ResetPW', - localize('could not connect to:') - ); - } - } - }; - request.send(null); - } catch (err) { - errorCall.call(this, err.toString(), 'Snap!Cloud'); - } -}; - -Cloud.prototype.login = function ( - username, - password, - callBack, - errorCall -) { - // both callBack and errorCall are two-argument functions - var request = new XMLHttpRequest(), - usr = JSON.stringify({'__h': password, '__u': username}), - myself = this; - this.setRoute(username); - try { - request.open( - "POST", - (this.hasProtocol() ? '' : 'http://') + - this.url + - '?SESSIONGLUE=' + - this.route, - true - ); - request.setRequestHeader( - "Content-Type", - "application/json; charset=utf-8" - ); - // glue this session to a route: - request.setRequestHeader('SESSIONGLUE', this.route); - request.withCredentials = true; - request.onreadystatechange = function () { - if (request.readyState === 4) { - if (request.responseText) { - myself.api = myself.parseAPI(request.responseText); - myself.session = request.getResponseHeader('MioCracker') - .split(';')[0]; - // set the cookie identifier: - myself.limo = this.getResponseHeader("miocracker") - .substring( - 9, - this.getResponseHeader("miocracker").indexOf("=") - ); - if (myself.api.logout) { - myself.username = username; - myself.password = password; - callBack.call(null, myself.api, 'Snap!Cloud'); - } else { - errorCall.call( - null, - request.responseText, - 'connection failed' - ); - } - } else { - errorCall.call( - null, - myself.url, - localize('could not connect to:') - ); - } - } - }; - request.send(usr); - } catch (err) { - errorCall.call(this, err.toString(), 'Snap!Cloud'); - } -}; - -Cloud.prototype.reconnect = function ( - callBack, - errorCall -) { - if (!(this.username && this.password)) { - this.message('You are not logged in'); - return; - } - this.login( - this.username, - this.password, - callBack, - errorCall - ); -}; - -Cloud.prototype.saveProject = function (ide, callBack, errorCall) { - var myself = this, - pdata, - media, - size, - mediaSize; - - ide.serializer.isCollectingMedia = true; - pdata = ide.serializer.serialize(ide.stage); - media = ide.hasChangedMedia ? - ide.serializer.mediaXML(ide.projectName) : null; - ide.serializer.isCollectingMedia = false; - ide.serializer.flushMedia(); - - mediaSize = media ? media.length : 0; - size = pdata.length + mediaSize; - if (mediaSize > 10485760) { - new DialogBoxMorph().inform( - 'Snap!Cloud - Cannot Save Project', - 'The media inside this project exceeds 10 MB.\n' + - 'Please reduce the size of costumes or sounds.\n', - ide.world(), - ide.cloudIcon(null, new Color(180, 0, 0)) - ); - throw new Error('Project media exceeds 10 MB size limit'); - } - - // check if serialized data can be parsed back again - try { - ide.serializer.parse(pdata); - } catch (err) { - ide.showMessage('Serialization of program data failed:\n' + err); - throw new Error('Serialization of program data failed:\n' + err); - } - if (media !== null) { - try { - ide.serializer.parse(media); - } catch (err) { - ide.showMessage('Serialization of media failed:\n' + err); - throw new Error('Serialization of media failed:\n' + err); - } - } - ide.serializer.isCollectingMedia = false; - ide.serializer.flushMedia(); - - ide.showMessage('Uploading ' + Math.round(size / 1024) + ' KB...'); - myself.reconnect( - function () { - myself.callService( - 'saveProject', - function (response, url) { - callBack.call(null, response, url); - myself.disconnect(); - ide.hasChangedMedia = false; - }, - errorCall, - [ - ide.projectName, - pdata, - media, - pdata.length, - media ? media.length : 0 - ] - ); - }, - errorCall - ); -}; - -Cloud.prototype.getProjectList = function (callBack, errorCall) { - var myself = this; - this.reconnect( - function () { - myself.callService( - 'getProjectList', - function (response, url) { - callBack.call(null, response, url); - myself.disconnect(); - }, - errorCall - ); - }, - errorCall - ); -}; - -Cloud.prototype.changePassword = function ( - oldPW, - newPW, - callBack, - errorCall -) { - var myself = this; - this.reconnect( - function () { - myself.callService( - 'changePassword', - function (response, url) { - callBack.call(null, response, url); - myself.disconnect(); - }, - errorCall, - [hex_sha512(oldPW), hex_sha512(newPW)] - ); - }, - errorCall - ); -}; - -Cloud.prototype.logout = function (callBack, errorCall) { - this.clear(); - this.callService( - 'logout', - callBack, - errorCall - ); -}; - -Cloud.prototype.disconnect = function () { - this.callService( - 'logout', - nop, - nop - ); -}; - -// Cloud: backend communication - -Cloud.prototype.callURL = function (url, callBack, errorCall) { - // both callBack and errorCall are optional two-argument functions - var request = new XMLHttpRequest(), - stickyUrl, - myself = this; - try { - // set the Limo. Also set the glue as a query paramter for backup. - stickyUrl = url + - '&SESSIONGLUE=' + - this.route + - '&_Limo=' + - this.limo; - request.open('GET', stickyUrl, true); - request.withCredentials = true; - request.setRequestHeader( - "Content-Type", - "application/x-www-form-urlencoded" - ); - request.setRequestHeader('MioCracker', this.session); - // Set the glue as a request header. - request.setRequestHeader('SESSIONGLUE', this.route); - request.onreadystatechange = function () { - if (request.readyState === 4) { - if (request.responseText) { - var responseList = myself.parseResponse( - request.responseText - ); - callBack.call(null, responseList, url); - } else { - errorCall.call( - null, - url, - 'no response from:' - ); - } - } - }; - request.send(null); - } catch (err) { - errorCall.call(this, err.toString(), url); - } -}; - -Cloud.prototype.callService = function ( - serviceName, - callBack, - errorCall, - args -) { - // both callBack and errorCall are optional two-argument functions - var request = new XMLHttpRequest(), - service = this.api[serviceName], - myself = this, - stickyUrl, - postDict; - - if (!this.session) { - errorCall.call(null, 'You are not connected', 'Cloud'); - return; - } - if (!service) { - errorCall.call( - null, - 'service ' + serviceName + ' is not available', - 'API' - ); - return; - } - if (args && args.length > 0) { - postDict = {}; - service.parameters.forEach(function (parm, idx) { - postDict[parm] = args[idx]; - }); - } - try { - stickyUrl = this.url + - '/' + - service.url + - '&SESSIONGLUE=' + - this.route + - '&_Limo=' + - this.limo; - request.open(service.method, stickyUrl, true); - request.withCredentials = true; - request.setRequestHeader( - "Content-Type", - "application/x-www-form-urlencoded" - ); - request.setRequestHeader('MioCracker', this.session); - request.setRequestHeader('SESSIONGLUE', this.route); - request.onreadystatechange = function () { - if (request.readyState === 4) { - var responseList = []; - if (request.responseText && - request.responseText.indexOf('ERROR') === 0) { - errorCall.call( - this, - request.responseText, - localize('Service:') + ' ' + localize(serviceName) - ); - return; - } - if (serviceName === 'login') { - myself.api = myself.parseAPI(request.responseText); - } - if (serviceName === 'getRawProject') { - responseList = request.responseText; - } else { - responseList = myself.parseResponse( - request.responseText - ); - } - callBack.call(null, responseList, service.url); - } - }; - request.send(this.encodeDict(postDict)); - } catch (err) { - errorCall.call(this, err.toString(), service.url); - } -}; - -// Cloud: payload transformation - -Cloud.prototype.parseAPI = function (src) { - var api = {}, - services; - services = src.split(" "); - services.forEach(function (service) { - var entries = service.split("&"), - serviceDescription = {}, - parms; - entries.forEach(function (entry) { - var pair = entry.split("="), - key = decodeURIComponent(pair[0]).toLowerCase(), - val = decodeURIComponent(pair[1]); - if (key === "service") { - api[val] = serviceDescription; - } else if (key === "parameters") { - parms = val.split(","); - if (!(parms.length === 1 && !parms[0])) { - serviceDescription.parameters = parms; - } - } else { - serviceDescription[key] = val; - } - }); - }); - return api; -}; - -Cloud.prototype.parseResponse = function (src) { - var ans = [], - lines; - if (!src) {return ans; } - lines = src.split(" "); - lines.forEach(function (service) { - var entries = service.split("&"), - dict = {}; - entries.forEach(function (entry) { - var pair = entry.split("="), - key = decodeURIComponent(pair[0]), - val = decodeURIComponent(pair[1]); - dict[key] = val; - }); - ans.push(dict); - }); - return ans; -}; - -Cloud.prototype.parseDict = function (src) { - var dict = {}; - if (!src) {return dict; } - src.split("&").forEach(function (entry) { - var pair = entry.split("="), - key = decodeURIComponent(pair[0]), - val = decodeURIComponent(pair[1]); - dict[key] = val; - }); - return dict; -}; - -Cloud.prototype.encodeDict = function (dict) { - var str = '', - pair, - key; - if (!dict) {return null; } - for (key in dict) { - if (dict.hasOwnProperty(key)) { - pair = encodeURIComponent(key) - + '=' - + encodeURIComponent(dict[key]); - if (str.length > 0) { - str += '&'; - } - str += pair; - } - } - return str; -}; - -// Cloud: user messages (to be overridden) - -Cloud.prototype.message = function (string) { - alert(string); -}; +/* + + cloud.js + + a backend API for SNAP! + + written by Bernat Romagosa + inspired in the old cloud API by Jens Mönig + + Copyright (C) 2017 by Bernat Romagosa + Copyright (C) 2015 by Jens Mönig + + This file is part of Snap!. + + Snap! is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +*/ + +// Global settings ///////////////////////////////////////////////////// + +/*global modules, IDE_Morph, SnapSerializer, nop, localize*/ + +modules.cloud = '2015-December-15'; + +// Global stuff + +var Cloud; + +// Cloud ///////////////////////////////////////////////////////////// + +function Cloud(url) { + this.init(url); +}; + +Cloud.prototype.init = function (url) { + this.url = url; + this.username = null; +}; + + +// Dictionary handling + +Cloud.prototype.parseDict = function (src) { + var dict = {}; + if (!src) {return dict; } + src.split("&").forEach(function (entry) { + var pair = entry.split("="), + key = decodeURIComponent(pair[0]), + val = decodeURIComponent(pair[1]); + dict[key] = val; + }); + return dict; +}; + +Cloud.prototype.encodeDict = function (dict) { + var str = '', + pair, + key; + if (!dict) {return null; } + for (key in dict) { + if (dict.hasOwnProperty(key)) { + pair = encodeURIComponent(key) + + '=' + + encodeURIComponent(dict[key]); + if (str.length > 0) { + str += '&'; + } + str += pair; + } + } + return str; +}; + +// Error handling + +Cloud.genericErrorMessage = + 'There was an error while trying to access\n' + + 'a Snap!Cloud service. Please try again later.'; + +Cloud.prototype.genericError = function () { + throw new Error(Cloud.genericErrorMessage); +}; + + +// Low level functionality + +Cloud.prototype.request = function ( + method, + path, + onSuccess, + onError, + errorMsg, + wantsRawResponse, + body) { + + var request = new XMLHttpRequest(), + myself = this; + + try { + request.open( + method, + this.url + path, + true + ); + request.setRequestHeader( + 'Content-Type', + 'application/json; charset=utf-8' + ); + request.withCredentials = true; + request.onreadystatechange = function () { + if (request.readyState === 4) { + if (request.responseText) { + var response = + (!wantsRawResponse || + (request.responseText.indexOf('{"errors"') === 0)) ? + JSON.parse(request.responseText) : + request.responseText; + + if (response.errors) { + onError.call( + null, + response.errors[0], + errorMsg + ); + } else { + if (onSuccess) { + onSuccess.call(null, response.message || response); + } + } + } else { + if (onError) { + onError.call( + null, + errorMsg || Cloud.genericErrorMessage, + myself.url + ); + } else { + myself.genericError(); + } + } + } + }; + request.send(body); + } catch (err) { + onError.call(this, err.toString(), 'Cloud Error'); + } +}; + +Cloud.prototype.withCredentialsRequest = function ( + method, + path, + onSuccess, + onError, + errorMsg, + wantsRawResponse, + body) { + + var myself = this; + this.checkCredentials( + function (username) { + if (username) { + myself.request( + method, + // %username is replaced by the actual username + path.replace('%username', username), + onSuccess, + onError, + errorMsg, + wantsRawResponse, + body); + } else { + onError.call(this, 'You are not logged in', 'Snap!Cloud'); + } + } + ); +}; + + +// Credentials management + +Cloud.prototype.initSession = function (onSuccess) { + var myself = this; + this.request( + 'POST', + '/init', + function () { myself.checkCredentials(onSuccess); }, + nop, + null, + true + ); +}; + +Cloud.prototype.checkCredentials = function (onSuccess, onError) { + var myself = this; + this.getCurrentUser( + function (user) { + if (user.username) { + myself.username = user.username; + } + if (onSuccess) { onSuccess.call(null, user.username, user.isadmin); } + }, + onError + ); +}; + +Cloud.prototype.getCurrentUser = function (onSuccess, onError) { + this.request('GET', '/users/c', onSuccess, onError, 'Could not retrieve current user'); +}; + +Cloud.prototype.getUser = function (username, onSuccess, onError) { + this.request('GET', '/users/' + username, onSuccess, onError, 'Could not retrieve user'); +}; + +Cloud.prototype.logout = function (onSuccess, onError) { + this.username = null; + this.request( + 'POST', + '/logout', + onSuccess, + onError, + 'logout failed' + ); +}; + +Cloud.prototype.login = function (username, password, persist, onSuccess, onError) { + var myself = this; + this.request( + 'POST', + '/users/' + username + '/login?' + + this.encodeDict({ + persist: persist + }), + function () { + myself.checkCredentials(onSuccess, onError); + }, + onError, + 'login failed', + 'false', // wants raw response + hex_sha512(password) // password travels inside the body + ); +}; + +Cloud.prototype.signup = function (username, password, passwordRepeat, email, onSuccess, onError) { + this.request( + 'POST', + '/users/' + username + '?' + this.encodeDict({ + email: email, + password: hex_sha512(password), + password_repeat: hex_sha512(passwordRepeat) + }), + onSuccess, + onError, + 'signup failed'); +}; + +Cloud.prototype.changePassword = function (password, newPassword, passwordRepeat, onSuccess, onError) { + this.withCredentialsRequest( + 'POST', + '/users/%username/newpassword?' + this.encodeDict({ + oldpassword: hex_sha512(password), + password_repeat: hex_sha512(passwordRepeat), + newpassword: hex_sha512(newPassword) + }), + onSuccess, + onError, + 'Could not change password' + ); + +}; + +// Projects + +Cloud.prototype.saveProject = function (ide, onSuccess, onError) { + var myself = this; + this.checkCredentials( + function (username) { + if (username) { + var xml = ide.serializer.serialize(ide.stage), + thumbnail = ide.stage.thumbnail( + SnapSerializer.prototype.thumbnailSize).toDataURL(), + body, mediaSize, size; + + ide.serializer.isCollectingMedia = true; + body = { + notes: ide.projectNotes, + xml: xml, + media: ide.hasChangedMedia ? + ide.serializer.mediaXML(ide.projectName) : null, + thumbnail: thumbnail + }; + ide.serializer.isCollectingMedia = false; + ide.serializer.flushMedia(); + + mediaSize = body.media ? body.media.length : 0; + size = body.xml.length + mediaSize; + if (mediaSize > 10485760) { + new DialogBoxMorph().inform( + 'Snap!Cloud - Cannot Save Project', + 'The media inside this project exceeds 10 MB.\n' + + 'Please reduce the size of costumes or sounds.\n', + ide.world(), + ide.cloudIcon(null, new Color(180, 0, 0)) + ); + throw new Error('Project media exceeds 10 MB size limit'); + } + + // check if serialized data can be parsed back again + try { + ide.serializer.parse(body.xml); + } catch (err) { + ide.showMessage('Serialization of program data failed:\n' + err); + throw new Error('Serialization of program data failed:\n' + err); + } + if (body.media !== null) { + try { + ide.serializer.parse(body.media); + } catch (err) { + ide.showMessage('Serialization of media failed:\n' + err); + throw new Error('Serialization of media failed:\n' + err); + } + } + ide.serializer.isCollectingMedia = false; + ide.serializer.flushMedia(); + + ide.showMessage('Uploading ' + Math.round(size / 1024) + ' KB...'); + + myself.request( + 'POST', + '/projects/' + username + '/' + ide.projectName, + onSuccess, + onError, + 'Project could not be saved', + false, + JSON.stringify(body), // POST body + ); + } else { + onError.call(this, 'You are not logged in', 'Snap!Cloud'); + } + } + ); +}; + +Cloud.prototype.getProjectList = function (onSuccess, onError, withThumbnail) { + var path = '/projects/%username'; + + if (withThumbnail) { + path += '?withthumbnail=true'; + } + + this.withCredentialsRequest( + 'GET', + path, + onSuccess, + onError, + 'Could not fetch projects' + ); +}; + +Cloud.prototype.getPublishedProjectList = function (username, page, pageSize, searchTerm, onSuccess, onError, withThumbnail) { + var path = '/projects' + (username ? '/' + username : '') + '?ispublished=true'; + + if (withThumbnail) { + path += '&withthumbnail=true'; + } + + if (page) { + path += '&page=' + page + '&pagesize=' + (pageSize || 16); + } + + if (searchTerm) { + path += '&matchtext=' + searchTerm; + } + + this.request( + 'GET', + path, + onSuccess, + onError, + 'Could not fetch projects' + ); +}; + +Cloud.prototype.getThumbnail = function (username, projectName, onSuccess, onError) { + this[username ? 'request' : 'withCredentialsRequest']( + 'GET', + '/projects/' + (username || '%username') + '/' + + projectName + '/thumbnail', + onSuccess, + onError, + 'Could not fetch thumbnail', + true + ); +}; + +Cloud.prototype.getProject = function (projectName, onSuccess, onError) { + this.withCredentialsRequest( + 'GET', + '/projects/%username/' + projectName, + onSuccess, + onError, + 'Could not fetch project ' + projectName, + true + ); +}; + +Cloud.prototype.getPublicProject = function (projectName, username, onSuccess, onError) { + this.request( + 'GET', + '/projects/' + username + '/' + projectName, + onSuccess, + onError, + 'Could not fetch project ' + projectName, + true + ); +}; + +Cloud.prototype.getProjectMetadata = function (projectName, username, onSuccess, onError) { + this.request( + 'GET', + '/projects/' + username + '/' + projectName + '/metadata', + onSuccess, + onError, + 'Could not fetch metadata for ' + projectName + ); +}; + +Cloud.prototype.deleteProject = function (projectName, username, onSuccess, onError) { + this[username ? 'request' : 'withCredentialsRequest']( + 'DELETE', + '/projects/' + (username || '%username') + '/' + projectName, + onSuccess, + onError, + 'Could not delete project' + ); +}; + +Cloud.prototype.shareProject = function (projectName, username, onSuccess, onError) { + this[username ? 'request' : 'withCredentialsRequest']( + 'POST', + '/projects/' + (username || '%username') + '/' + + projectName + '/metadata?ispublic=true', + onSuccess, + onError, + 'Could not share project' + ); +}; + +Cloud.prototype.unshareProject = function (projectName, username, onSuccess, onError) { + this[username ? 'request' : 'withCredentialsRequest']( + 'POST', + '/projects/' + (username || '%username') + '/' + + projectName + '/metadata?ispublic=false&ispublished=false', + onSuccess, + onError, + 'Could not unshare project' + ); +}; + +Cloud.prototype.publishProject = function (projectName, username, onSuccess, onError) { + this[username ? 'request' : 'withCredentialsRequest']( + 'POST', + '/projects/' + (username || '%username') + '/' + + projectName + '/metadata?ispublished=true', + onSuccess, + onError, + 'Could not publish project' + ); +}; + +Cloud.prototype.unpublishProject = function (projectName, username, onSuccess, onError) { + this[username ? 'request' : 'withCredentialsRequest']( + 'POST', + '/projects/' + (username || '%username') + '/' + + projectName + '/metadata?ispublished=false', + onSuccess, + onError, + 'Could not unpublish project' + ); +}; + +Cloud.prototype.updateNotes = function (projectName, notes, onSuccess, onError) { + this.withCredentialsRequest( + 'POST', + '/projects/%username/' + projectName + '/metadata', + onSuccess, + onError, + 'Could not update project notes', + false, // wants raw response + JSON.stringify({ notes: notes }) + ); +}; + +var SnapCloud = new Cloud('https://snap-cloud.cs10.org'); diff --git a/gui.js b/gui.js index 83e0fd8f..5d9441f9 100644 --- a/gui.js +++ b/gui.js @@ -241,6 +241,10 @@ IDE_Morph.prototype.init = function (isAutoFill) { this.corralBar = null; this.corral = null; + this.embedPlayButton = null; + this.embedOverlay = null; + this.isEmbedMode = false; + this.isAutoFill = isAutoFill === undefined ? true : isAutoFill; this.isAppMode = false; this.isSmallStage = false; @@ -268,20 +272,13 @@ IDE_Morph.prototype.init = function (isAutoFill) { IDE_Morph.prototype.openIn = function (world) { var hash, usr, myself = this, urlLanguage = null; - // get persistent user data, if any - if (this.hasLocalStorage()) { - usr = localStorage['-snap-user']; - if (usr) { - usr = SnapCloud.parseResponse(usr)[0]; - if (usr) { - SnapCloud.username = usr.username || null; - SnapCloud.password = usr.password || null; - if (SnapCloud.username) { - this.source = 'cloud'; - } + SnapCloud.initSession( + function (username) { + if (username) { + myself.source = 'cloud'; } } - } + ); this.buildPanes(); world.add(this); @@ -328,7 +325,10 @@ IDE_Morph.prototype.openIn = function (world) { } } - function applyFlags(dict) { + function applyFlags(dict) { + if (dict.embedMode) { + myself.setEmbedMode(); + } if (dict.editMode) { myself.toggleAppMode(false); } else { @@ -344,7 +344,7 @@ IDE_Morph.prototype.openIn = function (world) { if (dict.noExitWarning) { window.onbeforeunload = nop; } - } + } // dynamic notifications from non-source text files // has some issues, commented out for now @@ -405,7 +405,8 @@ IDE_Morph.prototype.openIn = function (world) { dict.Username = dict.Username.toLowerCase(); SnapCloud.getPublicProject( - SnapCloud.encodeDict(dict), + dict.ProjectName, + dict.Username, function (projectData) { var msg; myself.nextSteps([ @@ -442,10 +443,10 @@ IDE_Morph.prototype.openIn = function (world) { // make sure to lowercase the username dict = SnapCloud.parseDict(location.hash.substr(7)); - dict.Username = dict.Username.toLowerCase(); SnapCloud.getPublicProject( - SnapCloud.encodeDict(dict), + dict.ProjectName, + dict.Username, function (projectData) { var msg; myself.nextSteps([ @@ -478,10 +479,10 @@ IDE_Morph.prototype.openIn = function (world) { // make sure to lowercase the username dict = SnapCloud.parseDict(location.hash.substr(4)); - dict.Username = dict.Username.toLowerCase(); SnapCloud.getPublicProject( - SnapCloud.encodeDict(dict), + dict.ProjectName, + dict.Username, function (projectData) { myself.saveXMLAs(projectData, dict.ProjectName); myself.showMessage( @@ -1724,7 +1725,24 @@ IDE_Morph.prototype.fixLayout = function (situation) { if (situation !== 'refreshPalette') { // stage - if (this.isAppMode) { + if (this.isEmbedMode) { + this.stage.setScale(Math.floor(Math.min( + this.width() / this.stage.dimensions.x, + this.height() / this.stage.dimensions.y + ) * 10) / 10); + + this.embedPlayButton.size = Math.floor(Math.min( + this.width(), this.height())) / 3; + this.embedPlayButton.setWidth(this.embedPlayButton.size); + this.embedPlayButton.setHeight(this.embedPlayButton.size); + + if (this.embedOverlay) { + this.embedOverlay.setExtent(this.extent()); + } + + this.stage.setCenter(this.center()); + this.embedPlayButton.setCenter(this.center()); + } else if (this.isAppMode) { this.stage.setScale(Math.floor(Math.min( (this.width() - padding * 2) / this.stage.dimensions.x, (this.height() - this.controlBar.height() * 2 - padding * 2) @@ -2559,15 +2577,12 @@ IDE_Morph.prototype.cloudMenu = function () { function () { myself.prompt('Author name…', function (usr) { myself.prompt('Project name...', function (prj) { - var id = 'Username=' + - encodeURIComponent(usr.toLowerCase()) + - '&ProjectName=' + - encodeURIComponent(prj); myself.showMessage( 'Fetching project\nfrom the cloud...' ); SnapCloud.getPublicProject( - id, + prj, + usr, function (projectData) { var msg; if (!Process.prototype.isCatchingErrors) { @@ -4659,6 +4674,28 @@ IDE_Morph.prototype.toggleSliderExecute = function () { !ArgMorph.prototype.executeOnSliderEdit; }; +IDE_Morph.prototype.setEmbedMode = function () { + var myself = this; + this.embedOverlay = new Morph(); + this.embedOverlay.color = new Color(128, 128, 128); + this.embedOverlay.alpha = 0.5; + + this.embedPlayButton = new SymbolMorph('pointRight'); + this.embedPlayButton.color = new Color(128, 255, 128); + + this.embedPlayButton.mouseClickLeft = function () { + myself.runScripts(); + myself.embedOverlay.destroy(); + this.destroy(); + }; + + this.isEmbedMode = true; + this.controlBar.hide(); + this.add(this.embedOverlay); + this.add(this.embedPlayButton); + this.fixLayout(); +}; + IDE_Morph.prototype.toggleAppMode = function (appMode) { var world = this.world(), elements = [ @@ -5095,21 +5132,11 @@ IDE_Morph.prototype.initializeCloud = function () { new DialogBoxMorph( null, function (user) { - var pwh = hex_sha512(user.password), - str; SnapCloud.login( user.username, - pwh, + user.password, + user.choice, function () { - if (user.choice) { - str = SnapCloud.encodeDict( - { - username: user.username, - password: pwh - } - ); - localStorage['-snap-user'] = str; - } myself.source = 'cloud'; myself.showMessage('now connected.', 2); }, @@ -5133,23 +5160,20 @@ IDE_Morph.prototype.initializeCloud = function () { IDE_Morph.prototype.createCloudAccount = function () { var myself = this, world = this.world(); -/* - // force-logout, commented out for now: - delete localStorage['-snap-user']; - SnapCloud.clear(); -*/ + new DialogBoxMorph( null, function (user) { SnapCloud.signup( user.username, + user.password, + user.passwordRepeat, user.email, function (txt, title) { new DialogBoxMorph().inform( title, txt + - '.\n\nAn e-mail with your password\n' + - 'has been sent to the address provided', + '.\n\nYou can now log in.', world, myself.cloudIcon(null, new Color(0, 180, 0)) ); @@ -5174,11 +5198,7 @@ IDE_Morph.prototype.createCloudAccount = function () { IDE_Morph.prototype.resetCloudPassword = function () { var myself = this, world = this.world(); -/* - // force-logout, commented out for now: - delete localStorage['-snap-user']; - SnapCloud.clear(); -*/ + new DialogBoxMorph( null, function (user) { @@ -5221,8 +5241,8 @@ IDE_Morph.prototype.changeCloudPassword = function () { SnapCloud.changePassword( user.oldpassword, user.password, + user.passwordRepeat, function () { - myself.logout(); myself.showMessage('password has been changed.', 2); }, myself.cloudError() @@ -5244,14 +5264,11 @@ IDE_Morph.prototype.changeCloudPassword = function () { IDE_Morph.prototype.logout = function () { var myself = this; - delete localStorage['-snap-user']; SnapCloud.logout( function () { - SnapCloud.clear(); myself.showMessage('disconnected.', 2); }, function () { - SnapCloud.clear(); myself.showMessage('disconnected.', 2); } ); @@ -5735,6 +5752,12 @@ ProjectDialogMorph.prototype.buildContents = function () { this.unshareButton = this.addButton('unshareProject', 'Unshare'); this.shareButton.hide(); this.unshareButton.hide(); + /* + this.publishButton = this.addButton('publishProject', 'Publish'); + this.unpublishButton = this.addButton('unpublishProject', 'Unpublish'); + this.publishButton.hide(); + this.unpublishButton.hide(); + */ this.deleteButton = this.addButton('deleteProject', 'Delete'); this.addButton('cancel', 'Cancel'); @@ -5882,16 +5905,8 @@ ProjectDialogMorph.prototype.buildFilterField = function () { myself.listField.elements = myself.projectList.filter(function (aProject) { - var name, - notes; - - if (aProject.ProjectName) { // cloud - name = aProject.ProjectName; - notes = aProject.Notes; - } else { // local or examples - name = aProject.name; + var name = aProject.projectname || aProject.name, notes = aProject.notes || ''; - } return name.toLowerCase().indexOf(text.toLowerCase()) > -1 || notes.toLowerCase().indexOf(text.toLowerCase()) > -1; @@ -5925,10 +5940,10 @@ ProjectDialogMorph.prototype.setSource = function (source) { msg = myself.ide.showMessage('Updating\nproject list...'); this.projectList = []; SnapCloud.getProjectList( - function (projectList) { + function (response) { // Don't show cloud projects if user has since switch panes. if (myself.source === 'cloud') { - myself.installCloudProjectList(projectList); + myself.installCloudProjectList(response.projects); } msg.destroy(); }, @@ -6019,6 +6034,10 @@ ProjectDialogMorph.prototype.setSource = function (source) { this.body.add(this.listField); this.shareButton.hide(); this.unshareButton.hide(); + /* + this.publishButton.hide(); + this.unpublishButton.hide(); + */ if (this.source === 'local') { this.deleteButton.show(); } else { // examples @@ -6058,9 +6077,9 @@ ProjectDialogMorph.prototype.getExamplesProjectList = function () { ProjectDialogMorph.prototype.installCloudProjectList = function (pl) { var myself = this; - this.projectList = pl || []; + this.projectList = pl[0] ? pl : []; this.projectList.sort(function (x, y) { - return x.ProjectName.toLowerCase() < y.ProjectName.toLowerCase() ? + return x.projectname.toLowerCase() < y.projectname.toLowerCase() ? -1 : 1; }); @@ -6069,15 +6088,19 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) { this.projectList, this.projectList.length > 0 ? function (element) { - return element.ProjectName || element; + return element.projectname || element; } : null, [ // format: display shared project names bold [ 'bold', - function (proj) {return proj.Public === 'true'; } + function (proj) { return proj.ispublic; } + ], + [ + 'italic', + function (proj) { return proj.ispublished; } ] ], - function () {myself.ok(); } + function () { myself.ok(); } ); this.fixListFieldItemColors(); this.listField.fixLayout = nop; @@ -6091,17 +6114,25 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) { this.listField.action = function (item) { if (item === undefined) {return; } if (myself.nameField) { - myself.nameField.setContents(item.ProjectName || ''); + myself.nameField.setContents(item.projectname || ''); } if (myself.task === 'open') { - myself.notesText.text = item.Notes || ''; + myself.notesText.text = item.notes || ''; myself.notesText.drawNew(); myself.notesField.contents.adjustBounds(); - myself.preview.texture = item.Thumbnail || null; - myself.preview.cachedTexture = null; + myself.preview.texture = ''; myself.preview.drawNew(); + // we ask for the thumbnail when selecting a project + SnapCloud.getThumbnail( + null, // username is implicit + item.projectname, + function (thumbnail) { + myself.preview.texture = thumbnail; + myself.preview.cachedTexture = null; + myself.preview.drawNew(); + }); (new SpeechBubbleMorph(new TextMorph( - localize('last changed') + '\n' + item.Updated, + localize('last changed') + '\n' + item.lastupdated, null, null, null, @@ -6112,12 +6143,25 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) { myself.preview.rightCenter().add(new Point(2, 0)) ); } - if (item.Public === 'true') { + if (item.ispublic) { myself.shareButton.hide(); myself.unshareButton.show(); + /* + if (item.ispublished) { + myself.publishButton.hide(); + myself.unpublishButton.show(); + } else { + myself.publishButton.show(); + myself.unpublishButton.hide(); + } + */ } else { myself.unshareButton.hide(); myself.shareButton.show(); + /* + myself.publishButton.hide(); + myself.unpublishButton.hide(); + */ } myself.buttons.fixLayout(); myself.fixLayout(); @@ -6175,30 +6219,21 @@ ProjectDialogMorph.prototype.openCloudProject = function (project) { ProjectDialogMorph.prototype.rawOpenCloudProject = function (proj) { var myself = this; - SnapCloud.reconnect( - function () { - SnapCloud.callService( - 'getRawProject', - function (response) { - SnapCloud.disconnect(); - /* - if (myself.world().currentKey === 16) { - myself.ide.download(response); - return; - } - */ - myself.ide.source = 'cloud'; - myself.ide.droppedText(response); - if (proj.Public === 'true') { - location.hash = '#present:Username=' + - encodeURIComponent(SnapCloud.username) + - '&ProjectName=' + - encodeURIComponent(proj.ProjectName); - } - }, - myself.ide.cloudError(), - [proj.ProjectName] - ); + SnapCloud.getProject( + proj.projectname, + function (clouddata) { + myself.ide.source = 'cloud'; + myself.ide.nextSteps([ + function () { + myself.ide.openCloudDataString(clouddata); + } + ]); + if (proj.ispublic) { + location.hash = '#present:Username=' + + encodeURIComponent(SnapCloud.username) + + '&ProjectName=' + + encodeURIComponent(proj.projectname); + } }, myself.ide.cloudError() ); @@ -6211,11 +6246,12 @@ ProjectDialogMorph.prototype.saveProject = function () { myself = this; this.ide.projectNotes = notes || this.ide.projectNotes; + if (name) { if (this.source === 'cloud') { if (detect( this.projectList, - function (item) {return item.ProjectName === name; } + function (item) {return item.projectName === name; } )) { this.ide.confirm( localize( @@ -6284,26 +6320,20 @@ ProjectDialogMorph.prototype.deleteProject = function () { this.ide.confirm( localize( 'Are you sure you want to delete' - ) + '\n"' + proj.ProjectName + '"?', + ) + '\n"' + proj.projectname + '"?', 'Delete Project', function () { - SnapCloud.reconnect( + SnapCloud.deleteProject( + proj.projectname, + null, // username is implicit function () { - SnapCloud.callService( - 'deleteProject', - function () { - SnapCloud.disconnect(); - myself.ide.hasChangedMedia = true; - idx = myself.projectList.indexOf(proj); - myself.projectList.splice(idx, 1); - myself.installCloudProjectList( - myself.projectList - ); // refresh list - }, - myself.ide.cloudError(), - [proj.ProjectName] - ); - }, + myself.ide.hasChangedMedia = true; + idx = myself.projectList.indexOf(proj); + myself.projectList.splice(idx, 1); + myself.installCloudProjectList( + myself.projectList + ); // refresh list + }, myself.ide.cloudError() ); } @@ -6335,37 +6365,36 @@ ProjectDialogMorph.prototype.shareProject = function () { if (proj) { this.ide.confirm( localize( - 'Are you sure you want to publish' - ) + '\n"' + proj.ProjectName + '"?', + 'Are you sure you want to share' + ) + '\n"' + proj.projectname + '"?', 'Share Project', function () { myself.ide.showMessage('sharing\nproject...'); - SnapCloud.reconnect( + SnapCloud.shareProject( + proj.projectname, + null, // username is implicit function () { - SnapCloud.callService( - 'publishProject', - function () { - SnapCloud.disconnect(); - proj.Public = 'true'; - myself.unshareButton.show(); - myself.shareButton.hide(); - entry.label.isBold = true; - entry.label.drawNew(); - entry.label.changed(); - myself.buttons.fixLayout(); - myself.drawNew(); - myself.ide.showMessage('shared.', 2); - }, - myself.ide.cloudError(), - [proj.ProjectName] - ); + proj.ispublic = true; + myself.unshareButton.show(); + myself.shareButton.hide(); + /* + myself.publishButton.show(); + myself.unpublishButton.hide(); + */ + entry.label.isBold = true; + entry.label.drawNew(); + entry.label.changed(); + myself.buttons.fixLayout(); + myself.drawNew(); + myself.ide.showMessage('shared.', 2); + // Set the Shared URL if the project is currently open - if (proj.ProjectName === ide.projectName) { + if (proj.projectname === ide.projectName) { var usr = SnapCloud.username, projectId = 'Username=' + encodeURIComponent(usr.toLowerCase()) + '&ProjectName=' + - encodeURIComponent(proj.ProjectName); + encodeURIComponent(proj.projectname); location.hash = 'present:' + projectId; } }, @@ -6382,38 +6411,119 @@ ProjectDialogMorph.prototype.unshareProject = function () { proj = this.listField.selected, entry = this.listField.active; + if (proj) { + this.ide.confirm( + localize( + 'Are you sure you want to unshare' + ) + '\n"' + proj.projectname + '"?', + 'Unshare Project', + function () { + myself.ide.showMessage('unsharing\nproject...'); + SnapCloud.unshareProject( + proj.projectname, + null, // username is implicit + function () { + proj.ispublic = false; + myself.shareButton.show(); + myself.unshareButton.hide(); + /* + myself.publishButton.hide(); + myself.unpublishButton.hide(); + */ + entry.label.isBold = false; + entry.label.isItalic = false; + entry.label.drawNew(); + entry.label.changed(); + myself.buttons.fixLayout(); + myself.drawNew(); + myself.ide.showMessage('unshared.', 2); + if (proj.projectname === ide.projectName) { + location.hash = ''; + } + }, + myself.ide.cloudError() + ); + } + ); + } +}; + +ProjectDialogMorph.prototype.publishProject = function () { + var myself = this, + ide = this.ide, + proj = this.listField.selected, + entry = this.listField.active; + + if (proj) { + this.ide.confirm( + localize( + 'Are you sure you want to publish' + ) + '\n"' + proj.projectname + '"?', + 'Publish Project', + function () { + myself.ide.showMessage('publishing\nproject...'); + SnapCloud.publishProject( + proj.projectname, + null, // username is implicit + function () { + proj.ispublished = true; + myself.unshareButton.show(); + myself.shareButton.hide(); + myself.publishButton.hide(); + myself.unpublishButton.show(); + entry.label.isItalic = true; + entry.label.drawNew(); + entry.label.changed(); + myself.buttons.fixLayout(); + myself.drawNew(); + myself.ide.showMessage('published.', 2); + + // Set the Shared URL if the project is currently open + if (proj.projectname === ide.projectName) { + var usr = SnapCloud.username, + projectId = 'Username=' + + encodeURIComponent(usr.toLowerCase()) + + '&ProjectName=' + + encodeURIComponent(proj.projectname); + location.hash = 'present:' + projectId; + } + }, + myself.ide.cloudError() + ); + } + ); + } +}; + +ProjectDialogMorph.prototype.unpublishProject = function () { + var myself = this, + ide = this.ide, + proj = this.listField.selected, + entry = this.listField.active; if (proj) { this.ide.confirm( localize( 'Are you sure you want to unpublish' - ) + '\n"' + proj.ProjectName + '"?', - 'Unshare Project', + ) + '\n"' + proj.projectname + '"?', + 'Unpublish Project', function () { - myself.ide.showMessage('unsharing\nproject...'); - SnapCloud.reconnect( + myself.ide.showMessage('unpublishing\nproject...'); + SnapCloud.unpublishProject( + proj.projectname, + null, // username is implicit function () { - SnapCloud.callService( - 'unpublishProject', - function () { - SnapCloud.disconnect(); - proj.Public = 'false'; - myself.shareButton.show(); - myself.unshareButton.hide(); - entry.label.isBold = false; - entry.label.drawNew(); - entry.label.changed(); - myself.buttons.fixLayout(); - myself.drawNew(); - myself.ide.showMessage('unshared.', 2); - }, - myself.ide.cloudError(), - [proj.ProjectName] - ); - // Remove the shared URL if the project is open. - if (proj.ProjectName === ide.projectName) { - location.hash = ''; - } + proj.ispublished = false; + myself.unshareButton.show(); + myself.shareButton.hide(); + myself.publishButton.show(); + myself.unpublishButton.hide(); + entry.label.isItalic = false; + entry.label.drawNew(); + entry.label.changed(); + myself.buttons.fixLayout(); + myself.drawNew(); + myself.ide.showMessage('unpublished.', 2); }, myself.ide.cloudError() ); diff --git a/history.txt b/history.txt index 15ce0fd3..2297f874 100755 --- a/history.txt +++ b/history.txt @@ -1626,7 +1626,7 @@ ______ 130415 ------ -* Blocks: place sticky comments on World layer on dragging their anchor block +* Blocks: place sticky comments on World layer on dragging their anchor block 130416 ------ @@ -1704,7 +1704,7 @@ ______ 130514 ------ * paint.js: Paint editor, first version, contributed by Kartik Chandra, Yay!! -* Threads, Objects, Blocks: Broadcast & message enhancements: When I receive , and getLastMessage reporter + watcher +* Threads, Objects, Blocks: Broadcast & message enhancements: When I receive , and getLastMessage reporter + watcher 130515 ------ @@ -1726,7 +1726,7 @@ ______ 130605 ------ -* Objects: fix for hiding 'getLastAnswer' and 'getTimer' primitives +* Objects: fix for hiding 'getLastAnswer' and 'getTimer' primitives 130606 ------ @@ -1837,7 +1837,7 @@ ______ 130801 ------ * Blocks, Threads: "whitespace" & other options in SPLIT reporter's dropdown -* Blocks: Italicize editable input options (e.g. for the SPLT block) +* Blocks: Italicize editable input options (e.g. for the SPLT block) * Blocks: Undrop Reporters feature (in script areas' context menus) 130802 @@ -2245,7 +2245,7 @@ ______ * Objects, GUI: duplicate and clone nested sprites * GUI, Store: export and import nested sprites * Objects: double clicking on a sprite in the stage selects it in the IDE -* Objects: added ‘move’ option to the sprite context menu, lets the user move (nested) sprites in edit mode without changing their layering, and also sprites marked “undraggable” +* Objects: added ‘move’ option to the sprite context menu, lets the user move (nested) sprites in edit mode without changing their layering, and also sprites marked “undraggable” * updated Portuguese translation, thanks, Manuel! * updated German translation * Morphic: fixed #497 (prevent bubble shadows from getting cut-off) @@ -2285,7 +2285,7 @@ ______ 140930 ------ -* Objects: fixed #593 match broadcast numbers with event hat blocks containing strings that can be parsed as numbers +* Objects: fixed #593 match broadcast numbers with event hat blocks containing strings that can be parsed as numbers * BYOB: allow percent symbols in custom block texts (fix #361), thanks, @Gubolin!! * Morphic: allow negative min/max values for sliders (fix #285), thanks, @Gubolin!! * Objects: fixed #378 (disable context menus for boolean representations) @@ -2927,7 +2927,7 @@ http://snap.berkeley.edu/run#cloud:Username=jens&ProjectName=rotation * Updated Simplified Chinese translation, thanks to @ubertao! * Media import dialog with thumbnail, thanks to @ubertao! -== v4.0.7.2 ==== +== v4.0.7.2 ==== 160714 ------ @@ -3050,7 +3050,7 @@ http://snap.berkeley.edu/run#cloud:Username=jens&ProjectName=rotation 160924 ------ -* don’t update the recursion cache when updating a custom block definition +* don’t update the recursion cache when updating a custom block definition 160929 ------ @@ -3167,7 +3167,7 @@ http://snap.berkeley.edu/run#cloud:Username=jens&ProjectName=rotation 161206 ------ -* GUI: Switch to asynchronous loading of resources (costumes, sounds, libraries etc.) +* GUI: Switch to asynchronous loading of resources (costumes, sounds, libraries etc.) * Morphic: Added support for dropping links to SVGs from other web pages onto the World * GUI: Support importing unrasterized SVG_Costumes from the “Costumes” and “Backgrounds” dialog @@ -3348,7 +3348,7 @@ Fixes: 170201 ------ -* GUI: let costume icons indicate svg costumes +* GUI: let costume icons indicate svg costumes 170202 ------ @@ -3508,7 +3508,7 @@ Fixes: 170707 ------ * Objects, GUI, Store: tweak naming of instantiating to “clone”, enable inheritance by default -* Objects, GUI: run “When I start as clone” scripts when manually cloning a sprite, only position at hand pointer if no such scripts exist +* Objects, GUI: run “When I start as clone” scripts when manually cloning a sprite, only position at hand pointer if no such scripts exist * Morphic, Objects: confine turtle direction readout to 0-360 degrees, thanks, Cynthia for the bug report!! 170708 @@ -3518,7 +3518,7 @@ Fixes: 170709 ------ -* Objects, Threads: added experimental (only shown in dev mode) “tell ... to ..." and “ask ... for ...” primitives +* Objects, Threads: added experimental (only shown in dev mode) “tell ... to ..." and “ask ... for ...” primitives 170711 ------ @@ -3759,14 +3759,14 @@ v4.1 Features: Fixes: * changed keyboard shortcut indicator for “find blocks” to “^” -* prevent Snap from “hanging” when encountering certain errors in visible stepping +* prevent Snap from “hanging” when encountering certain errors in visible stepping * only mark implicit parameters if no formal ones exist * optimized thread-launch and script highlighting to a single frame instead of formerly two * changed direction attribute of sprites to automatically confine to 0-360 degrees * fixed rotation-bug when flipping costumes in "only turn left/right" mode" * fixed variable renaming (“refactoring”) bugs, thanks, Bernat! * fixed “fill” block crash when applying the same color twice -* fixed occasional empty drop-down menu items named “close” +* fixed occasional empty drop-down menu items named “close” * fixed some typos * limited sprites' direction and coordinates to finite numbers * made block vars transient for block libraries @@ -3925,6 +3925,10 @@ Translation Updates: * Turkish * Chinese * Spanish + + +=== development === +* New cloud API * Russian *** in development *** diff --git a/widgets.js b/widgets.js index 7d15fd1f..b15bc474 100644 --- a/widgets.js +++ b/widgets.js @@ -2075,6 +2075,10 @@ DialogBoxMorph.prototype.promptCredentials = function ( emlLabel = labelText('foo'); inp.add(emlLabel); inp.add(eml); + inp.add(labelText('Password:')); + inp.add(pw1); + inp.add(labelText('Repeat Password:')); + inp.add(pw2); } if (purpose === 'login') { @@ -2182,7 +2186,7 @@ DialogBoxMorph.prototype.promptCredentials = function ( if (purpose === 'login') { checklist = [usr, pw1]; } else if (purpose === 'signup') { - checklist = [usr, bmn, byr, eml]; + checklist = [usr, bmn, byr, eml, pw1, pw2]; } else if (purpose === 'changePassword') { checklist = [opw, pw1, pw2]; } else if (purpose === 'resetPassword') { @@ -2205,12 +2209,12 @@ DialogBoxMorph.prototype.promptCredentials = function ( return false; } if (em.indexOf(' ') > -1 || em.indexOf('@') === -1 - || em.indexOf('.') === -1) { + || em.indexOf('.') === -1 || em.length < 5) { indicate(eml, 'please provide a valid\nemail address'); return false; } } - if (purpose === 'changePassword') { + if (purpose === 'changePassword' || purpose === 'signup') { if (pw1.getValue().length < 6) { indicate(pw1, 'password must be six\ncharacters or longer'); return false; @@ -2249,6 +2253,7 @@ DialogBoxMorph.prototype.promptCredentials = function ( email: eml.getValue(), oldpassword: opw.getValue(), password: pw1.getValue(), + passwordRepeat: pw2.getValue(), choice: agree }; };