diff options
| -rw-r--r-- | cloud.js | 616 | ||||
| -rw-r--r-- | github.js | 273 | ||||
| -rw-r--r-- | gui.js | 721 | ||||
| -rwxr-xr-x | snap.html | 3 |
4 files changed, 1462 insertions, 151 deletions
@@ -1,11 +1,12 @@ /* -cloud.js - a GitHub backend API for SNAP! + cloud.js - written by Gubolin, based on cloud.js by Jens Mönig + a backend API for SNAP! - Copyright (C) 2014 by Jens Mönig, Gubolin + written by Jens Mönig + + Copyright (C) 2014 by Jens Mönig This file is part of Snap!. @@ -29,73 +30,253 @@ cloud.js /*global modules, IDE_Morph, SnapSerializer, hex_sha512, alert, nop, localize*/ -modules.cloud = '2014-July-29'; +modules.cloud = '2014-May-26'; // Global stuff var Cloud; -var SnapCloud = new Cloud(); +var SnapCloud = new Cloud( + 'https://snapcloud.miosoft.com/miocon/app/login?_app=SnapCloud' +); // Cloud ///////////////////////////////////////////////////////////// function Cloud(url) { - this.gh = null; this.username = null; - this.password = null; // TODO saved as plain text + this.password = null; // hex_sha512 hashed + this.url = url; this.session = null; this.api = {}; } Cloud.prototype.clear = function () { - this.gh = null; this.username = null; this.password = null; this.session = null; this.api = {}; }; +Cloud.prototype.hasProtocol = function () { + return this.url.toLowerCase().indexOf('http') === 0; +}; + // Cloud: Snap! API -Cloud.prototype.getProject = function ( - dict, +Cloud.prototype.signup = function ( + username, + email, callBack, errorCall ) { - var myself = this; - var projectName = dict.projectname; - var userName = dict.username; + // 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'); + } +}; - if (myself.gh !== null) { - var repo = myself.gh.getRepo(userName, projectName); - var branch = repo.getBranch(); // master (default) - var media, pdata; - var br; +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(), + responseList, + myself = this; + try { + request.open( + "GET", + (this.hasProtocol() ? '' : 'http://') + + this.url + 'Public' + + '&' + + 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 { + responseList = myself.parseResponse( + request.responseText + ); + callBack.call( + null, + responseList[0].SourceCode + ); + } + } else { + errorCall.call( + null, + myself.url + 'Public', + localize('could not connect to:') + ); + } + } + }; + request.send(null); + } catch (err) { + errorCall.call(this, err.toString(), 'Snap!Cloud'); + } +}; - branch.read('snap.xml', false).then( - function (sourceContent) { - branch.read('media.xml', false).then( // true for binary - function (mediaContent) { +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, - sourceContent.content, - mediaContent.content + request.responseText, + 'Reset Password' ); - }, - function (error) { - errorCall.call(this, error, 'Github'); } - ); - }, - function (error) { - errorCall.call(this, error, 'Github'); + } 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.connect = function ( + 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, + true + ); + request.setRequestHeader( + "Content-Type", + "application/x-www-form-urlencoded" ); - } else { - errorCall.call(null, 'Please login', 'Github'); + 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]; + if (myself.api.login) { + callBack.call(null, myself.api, 'Snap!Cloud'); + } else { + errorCall.call( + null, + 'connection failed' + ); + } + } else { + errorCall.call( + null, + myself.url, + localize('could not connect to:') + ); + } + } + }; + request.send(null); + } catch (err) { + errorCall.call(this, err.toString(), 'Snap!Cloud'); } }; + Cloud.prototype.login = function ( username, password, @@ -103,22 +284,61 @@ Cloud.prototype.login = function ( errorCall ) { var myself = this; + this.connect( + function () { + myself.rawLogin(username, password, callBack, errorCall); + myself.disconnect(); + }, + errorCall + ); +}; - myself.gh = new Octokit({ - username: username, - password: password - }); - - myself.gh.getUser().getInfo().then( - function(info) { - myself.username = username; - myself.password = password; - - callBack.call(myself); +Cloud.prototype.rawLogin = function ( + username, + password, + callBack, + errorCall +) { + // both callBack and errorCall are two-argument functions + var myself = this, + pwHash = hex_sha512("miosoft%20miocon," + + this.session.split('=')[1] + "," + + encodeURIComponent(username.toLowerCase()) + "," + + password // alreadey hex_sha512 hashed + ); + this.callService( + 'login', + function (response, url) { + if (myself.api.logout) { + myself.username = username; + myself.password = password; + callBack.call(null, response, url); + } else { + errorCall.call( + null, + 'Service catalog is not available,\nplease retry', + 'Connection Error:' + ); + } }, - function (error) { - errorCall.call(this, error, 'Github'); - } + errorCall, + [username, pwHash] + ); +}; + +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 ); }; @@ -126,7 +346,6 @@ Cloud.prototype.saveProject = function (ide, callBack, errorCall) { var myself = this, pdata, media; - var repoName = ide.projectName.replace(/[^\w-]/g, ''); // FIXME validation of project name ide.serializer.isCollectingMedia = true; pdata = ide.serializer.serialize(ide.stage); @@ -153,94 +372,215 @@ Cloud.prototype.saveProject = function (ide, callBack, errorCall) { ide.serializer.isCollectingMedia = false; ide.serializer.flushMedia(); - myself.getProjectList( - function (projects) { - var exists = false; - - projects.forEach(function (project) { - if (project.ProjectName.indexOf(repoName) > -1) { - exists = true; - return; - } - }); - - if (exists === false){ - myself.gh.getUser().createRepo(repoName, { // these should be discussed - 'description': 'Snap! Project', - 'has_wiki': 'false', - 'has_downloads': 'false', - 'auto_init': true, - 'license_template': 'mit' // discuss - }).then( - function () {}, - function (error) { - errorCall.call(this, error, 'Github'); - } - ); - } - - if (myself.gh !== null) { - var repo = myself.gh.getRepo(myself.username, ide.projectName); - var branch = repo.getBranch(); // master (default) - var message = ''; // FIXME optional: specify message - - var contents = { - 'snap.xml': pdata, - 'media.xml': media // may be binary - }; - - branch.writeMany(contents, message).then( - function () { - callBack.call() - }, - function (error) { - errorCall.call(this, error, 'Github'); - } - ); - } - }, - function (error) { - errorCall.call(null, error, 'Github'); - } + 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 + ); +}; - if (myself.gh !== null) { - myself.gh.getUser().getRepos().then( - function (repos) { - var snapProjects = []; +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, + [oldPW, newPW] + ); + }, + errorCall + ); +}; - repos.forEach(function (repo) { - if (repo.description === 'Snap! Project') { // FIXME nicer detection - var project; +Cloud.prototype.logout = function (callBack, errorCall) { + this.clear(); + this.callService( + 'logout', + callBack, + errorCall + ); +}; - project = { - 'ProjectName': repo.name - }; +Cloud.prototype.disconnect = function () { + this.callService( + 'logout', + nop, + nop + ); +}; - snapProjects.push(project); - } - }); +// Cloud: backend communication - callBack.call(myself, snapProjects); - }, - function (error) { - errorCall.call(this, error, 'Github'); - } +Cloud.prototype.callURL = function (url, callBack, errorCall) { + // both callBack and errorCall are optional two-argument functions + var request = new XMLHttpRequest(), + myself = this; + try { + request.open('GET', url, true); + request.withCredentials = true; + request.setRequestHeader( + "Content-Type", + "application/x-www-form-urlencoded" ); - } else { - errorCall.call(myself, 'Please login', 'Github'); + request.setRequestHeader('MioCracker', this.session); + 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.logout = function (callBack) { - this.clear(); +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, + 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 { + request.open(service.method, service.url, true); + request.withCredentials = true; + request.setRequestHeader( + "Content-Type", + "application/x-www-form-urlencoded" + ); + request.setRequestHeader('MioCracker', this.session); + 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); + } + 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: backend communication +// 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 = [], @@ -248,16 +588,16 @@ Cloud.prototype.parseResponse = function (src) { 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); + 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; }; @@ -266,8 +606,8 @@ Cloud.prototype.parseDict = function (src) { if (!src) {return dict; } src.split("&").forEach(function (entry) { var pair = entry.split("="), - key = decodeURIComponent(pair[0]), - val = decodeURIComponent(pair[1]); + key = decodeURIComponent(pair[0]), + val = decodeURIComponent(pair[1]); dict[key] = val; }); return dict; @@ -279,8 +619,10 @@ Cloud.prototype.encodeDict = function (dict) { key; if (!dict) {return null; } for (key in dict) { - if (dict.hasOwnProperty(key)) { - pair = encodeURIComponent(key) + '=' + encodeURIComponent(dict[key]); + if (dict.hasOwnProperty(key)) { + pair = encodeURIComponent(key) + + '=' + + encodeURIComponent(dict[key]); if (str.length > 0) { str += '&'; } @@ -289,3 +631,9 @@ Cloud.prototype.encodeDict = function (dict) { } return str; }; + +// Cloud: user messages (to be overridden) + +Cloud.prototype.message = function (string) { + alert(string); +}; diff --git a/github.js b/github.js new file mode 100644 index 0000000..a3b3223 --- /dev/null +++ b/github.js @@ -0,0 +1,273 @@ +/* + + github.js + + a GitHubBackend backend API for SNAP! + + written by Gubolin, based on cloud.js by Jens Mönig + + Copyright (C) 2014 by Jens Mönig, Gubolin + + 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 <http://www.gnu.org/licenses/>. + +*/ + +// Global settings ///////////////////////////////////////////////////// + +/*global modules, IDE_Morph, SnapSerializer, nop, +localize*/ + +modules.github = '2014-July-31'; + +// Global stuff + +var GitHubBackend; + +var GitHub = new GitHubBackend(); + +// GitHubBackend ///////////////////////////////////////////////////////////// + +function GitHubBackend(url) { + this.gh = null; + this.username = null; + this.password = null; // TODO saved as plain text +} + +GitHubBackend.prototype.clear = function () { + this.gh = null; + this.username = null; + this.password = null; +}; + +// GitHubBackend: Snap! API + +GitHubBackend.prototype.getProject = function ( + userName, + projectName, + callBack, + errorCall +) { + var myself = this; + + if (myself.gh === null) { + myself.gh = new Octokit(); + } + + var repo = myself.gh.getRepo(userName, projectName); + var branch = repo.getBranch(); // master (default) + var media, pdata; + + branch.read('snap.xml', false).then( + function (sourceContent) { + branch.read('media.xml', false).then( // true for binary + function (mediaContent) { + callBack.call( + null, + sourceContent.content, + mediaContent.content + ); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); +}; + +GitHubBackend.prototype.login = function ( + username, + password, + validateData, + callBack, + errorCall +) { + var myself = this; + var me; + + myself.gh = new Octokit({ + username: username, + password: password + }); + + if (validateData === true) { + me = myself.gh.getUser(); + if (me !== null) { + me.getInfo().then( + function(info) { + myself.username = username; + myself.password = password; + + callBack.call(myself); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } else { + errorCall.call(myself, localize('Something went wrong :('), 'GitHub'); + } + } else { + myself.username = username; + myself.password = password; + + callBack.call(myself); + } +}; + +GitHubBackend.prototype.saveProject = function (ide, callBack, errorCall) { + var myself = this, + pdata, + media; + var repoName = ide.projectName.replace(/[^\w-]/g, ''); // TODO validation of project name + + 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(); + + // 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(); + + myself.getProjectList( + function (projects) { + var exists = false; + + projects.forEach(function (project) { + if (project.ProjectName.indexOf(repoName) > -1) { + exists = true; + return; + } + }); + + if (exists === false){ + myself.gh.getUser().createRepo(repoName, { // these should be discussed + 'description': 'Snap! Project - http://snap.berkeley.edu/snapsource/snap.html#github:Username=' + myself.username + '&projectName=' + repoName, + 'has_wiki': 'false', + 'has_downloads': 'false', + 'auto_init': true, + 'license_template': 'mit' // discuss + }).then( + function () {}, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } + + if (myself.gh !== null) { + var repo = myself.gh.getRepo(myself.username, ide.projectName); + var branch = repo.getBranch(); // master (default) + var message = ''; // TODO optional: specify message + + var contents = { + 'snap.xml': pdata, + 'media.xml': media, // may be binary + 'README.md': ide.projectNotes + }; + + branch.writeMany(contents, message).then( + function () { + callBack.call(); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } + }, + function (error) { + errorCall.call(null, error, 'GitHub'); + } + ); +}; + +GitHubBackend.prototype.getProjectList = function (callBack, errorCall) { + var myself = this; + + if (myself.gh !== null) { + myself.gh.getUser().getRepos().then( + function (repos) { + var snapProjects = []; + + var modCallBack = (function () { + var called = 0; + return function () { + if (++called == repos.length) { + callBack.call(myself, snapProjects); + } + }; + })(); + + repos.forEach(function (repo) { + if (repo.description.indexOf('Snap! Project') > -1) { // TODO nicer detection + var project, ghrepo, branch; + + ghrepo = myself.gh.getRepo(repo.owner.login, repo.name); + branch = ghrepo.getBranch(); // master (default) + + branch.read('README.md', false).then( + function (notesContent) { + project = { + 'ProjectName': repo.name, + 'Notes': notesContent.content, + 'Updated': repo.updated_at.replace(/T/, ' ').replace(/Z/, '') // TODO this could be better + }; + + snapProjects.push(project); + modCallBack(); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } else { + modCallBack(); + } + }); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } else { + errorCall.call(myself, localize('Please login'), 'GitHub'); + } +}; + +GitHubBackend.prototype.logout = function (callBack) { + this.clear(); +}; @@ -63,13 +63,13 @@ Costume, CostumeEditorMorph, MorphicPreferences, touchScreenSettings, standardSettings, Sound, BlockMorph, ToggleMorph, InputSlotDialogMorph, ScriptsMorph, isNil, SymbolMorph, BlockExportDialogMorph, BlockImportDialogMorph, SnapTranslator, localize, List, InputSlotMorph, -SnapCloud, Uint8Array, HandleMorph, SVG_Costume, fontHeight, hex_sha512, +SnapCloud, GitHub, Uint8Array, HandleMorph, SVG_Costume, fontHeight, hex_sha512, sb, CommentMorph, CommandBlockMorph, BlockLabelPlaceHolderMorph, Audio, SpeechBubbleMorph*/ // Global stuff //////////////////////////////////////////////////////// -modules.gui = '2014-July-30'; +modules.gui = '2014-July-31'; // Declarations @@ -240,6 +240,7 @@ IDE_Morph.prototype.openIn = function (world) { // get persistent user data, if any if (localStorage) { usr = localStorage['-snap-user']; + ghusr = localStorage['-snap-ghuser']; if (usr) { usr = SnapCloud.parseResponse(usr)[0]; if (usr) { @@ -247,6 +248,13 @@ IDE_Morph.prototype.openIn = function (world) { function() {}, myself.cloudError()); } } + if (ghusr) { + ghusr = SnapCloud.parseResponse(ghusr)[0]; + if (ghusr) { + GitHub.login(ghusr.username, ghusr.password, false, + function() {}, myself.githubError()); + } + } } // override SnapCloud's user message with Morphic @@ -338,11 +346,12 @@ IDE_Morph.prototype.openIn = function (world) { this.parent.add(this.shield); myself.showMessage('Fetching project\nfrom the cloud...'); + // make sure to lowercase the username dict = SnapCloud.parseDict(location.hash.substr(9)); - dict.Username = dict.Username; + dict.Username = dict.Username.toLowerCase(); SnapCloud.getPublicProject( - dict, + SnapCloud.encodeDict(dict), function (projectData) { var msg; myself.nextSteps([ @@ -370,10 +379,51 @@ IDE_Morph.prototype.openIn = function (world) { }, this.cloudError() ); + } else if (location.hash.substr(0, 8) === '#github:') { + this.shield = new Morph(); + this.shield.color = this.color; + this.shield.setExtent(this.parent.extent()); + this.parent.add(this.shield); + myself.showMessage('Fetching project\nfrom GitHub...'); + + dict = SnapCloud.parseDict(location.hash.substr(8)); + + GitHub.getProject( + dict.Username, + dict.projectName, + function (projectData) { + var msg; + myself.nextSteps([ + function () { + msg = myself.showMessage('Opening GitHub project...'); + }, + function () { + if (projectData.indexOf('<snapdata') === 0) { + myself.rawOpenCloudDataString(projectData); + } else if ( + projectData.indexOf('<project') === 0 + ) { + myself.rawOpenProjectString(projectData); + } + myself.hasChangedMedia = true; + }, + function () { + myself.shield.destroy(); + myself.shield = null; + msg.destroy(); + myself.toggleAppMode(true); + myself.runScripts(); + } + ]); + }, + this.githubError() + ); } else if (location.hash.substr(0, 6) === '#lang:') { urlLanguage = location.hash.substr(6); this.setLanguage(urlLanguage); this.loadNewProject = true; + } else if (location.hash.substr(0, 7) === '#signup') { + this.createCloudAccount(); } } @@ -1934,16 +1984,49 @@ IDE_Morph.prototype.cloudMenu = function () { shiftClicked = (world.currentKey === 16); menu = new MenuMorph(this); + if (shiftClicked) { + menu.addItem( + 'url...', + 'setCloudURL', + null, + new Color(100, 0, 0) + ); + menu.addLine(); + } if (!SnapCloud.username) { menu.addItem( 'Login...', 'initializeCloud' ); + menu.addItem( + 'Signup...', + 'createCloudAccount' + ); + menu.addItem( + 'Reset Password...', + 'resetCloudPassword' + ); } else { menu.addItem( localize('Logout') + ' ' + SnapCloud.username, 'logout' ); + menu.addItem( + 'Change Password...', + 'changeCloudPassword' + ); + } + + if (!GitHub.username) { + menu.addItem( + 'Login via GitHub...', + 'initializeGitHub' + ); + } else { + menu.addItem( + localize('Logout') + ' ' + GitHub.username + ' ' + localize('from GitHub'), + 'logoutGitHub' + ); } if (shiftClicked) { menu.addLine(); @@ -1989,6 +2072,53 @@ IDE_Morph.prototype.cloudMenu = function () { null, new Color(100, 0, 0) ); + menu.addLine(); + menu.addItem( + 'open shared project from cloud...', + 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, + function (projectData) { + var msg; + if (!Process.prototype.isCatchingErrors) { + window.open( + 'data:text/xml,' + projectData + ); + } + myself.nextSteps([ + function () { + msg = myself.showMessage( + 'Opening project...' + ); + }, + function () { + myself.rawOpenCloudDataString( + projectData + ); + }, + function () { + msg.destroy(); + } + ]); + }, + myself.cloudError() + ); + + }, null, 'project'); + }, null, 'project'); + }, + null, + new Color(100, 0, 0) + ); } menu.popup(world, pos); }; @@ -2633,7 +2763,14 @@ IDE_Morph.prototype.editProjectNotes = function () { }; IDE_Morph.prototype.newProject = function () { - this.source = SnapCloud.username ? 'cloud' : 'local'; + if (SnapCloud.username) { + this.source = 'cloud'; + } else if (GitHub.username) { + this.source = 'github'; + } else { + this.source = 'local'; + } + if (this.stage) { this.stage.destroy(); } @@ -2665,8 +2802,10 @@ IDE_Morph.prototype.save = function () { if (this.projectName) { if (this.source === 'local') { // as well as 'examples' this.saveProject(this.projectName); - } else { // 'cloud' + } else if (this.source === 'cloud') { // 'cloud' this.saveProjectToCloud(this.projectName); + } else { // 'github' + this.saveProjectToGitHub(this.projectName); } } else { this.saveProjectsBrowser(); @@ -3554,17 +3693,17 @@ IDE_Morph.prototype.initializeCloud = function () { new DialogBoxMorph( null, function (user) { - var pw = user.password, + var pwh = hex_sha512(user.password), str; SnapCloud.login( user.username, - pw, + pwh, function () { if (user.choice) { str = SnapCloud.encodeDict( { username: user.username, - password: pw + password: pwh } ); localStorage['-snap-user'] = str; @@ -3589,6 +3728,160 @@ IDE_Morph.prototype.initializeCloud = function () { ); }; +IDE_Morph.prototype.initializeGitHub = function () { + var myself = this, + world = this.world(); + new DialogBoxMorph( + null, + function (user) { + var pw = user.password, + str; + GitHub.login( + user.username, + pw, + true, + function () { + if (user.choice) { + str = SnapCloud.encodeDict( + { + username: user.username, + password: pw + } + ); + localStorage['-snap-ghuser'] = str; + } + myself.source = 'github'; + myself.showMessage('now connected.', 2); + }, + myself.githubError() + ); + } + ).withKey('cloudlogin').promptCredentials( + 'Sign in with your GitHub account', + 'login', + null, + null, + null, + null, + 'stay signed in on this computer\nuntil logging out', + world, + myself.cloudIcon(), + myself.cloudMsg + ); +}; + +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.email, + function (txt, title) { + new DialogBoxMorph().inform( + title, + txt + + '.\n\nAn e-mail with your password\n' + + 'has been sent to the address provided', + world, + myself.cloudIcon(null, new Color(0, 180, 0)) + ); + }, + myself.cloudError() + ); + } + ).withKey('cloudsignup').promptCredentials( + 'Sign up', + 'signup', + 'http://snap.berkeley.edu/tos.html', + 'Terms of Service...', + 'http://snap.berkeley.edu/privacy.html', + 'Privacy...', + 'I have read and agree\nto the Terms of Service', + world, + myself.cloudIcon(), + myself.cloudMsg + ); +}; + +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) { + SnapCloud.resetPassword( + user.username, + function (txt, title) { + new DialogBoxMorph().inform( + title, + txt + + '.\n\nAn e-mail with a link to\n' + + 'reset your password\n' + + 'has been sent to the address provided', + world, + myself.cloudIcon(null, new Color(0, 180, 0)) + ); + }, + myself.cloudError() + ); + } + ).withKey('cloudresetpassword').promptCredentials( + 'Reset password', + 'resetPassword', + null, + null, + null, + null, + null, + world, + myself.cloudIcon(), + myself.cloudMsg + ); +}; + +IDE_Morph.prototype.changeCloudPassword = function () { + var myself = this, + world = this.world(); + new DialogBoxMorph( + null, + function (user) { + SnapCloud.changePassword( + user.oldpassword, + user.password, + function () { + myself.logout(); + myself.showMessage('password has been changed.', 2); + }, + myself.cloudError() + ); + } + ).withKey('cloudpassword').promptCredentials( + 'Change Password', + 'changePassword', + null, + null, + null, + null, + null, + world, + myself.cloudIcon(), + myself.cloudMsg + ); +}; + IDE_Morph.prototype.logout = function () { var myself = this; delete localStorage['-snap-user']; @@ -3602,6 +3895,19 @@ IDE_Morph.prototype.logout = function () { ); }; +IDE_Morph.prototype.logoutGitHub = function () { + var myself = this; + delete localStorage['-snap-ghuser']; + GitHub.logout( + function () { + myself.showMessage('disconnected.', 2); + }, + function () { + myself.showMessage('disconnected.', 2); + } + ); +}; + IDE_Morph.prototype.saveProjectToCloud = function (name) { var myself = this; if (name) { @@ -3615,6 +3921,19 @@ IDE_Morph.prototype.saveProjectToCloud = function (name) { } }; +IDE_Morph.prototype.saveProjectToGitHub = function (name) { + var myself = this; + if (name) { + this.showMessage('Saving project\nto GitHub...'); + this.setProjectName(name); + GitHub.saveProject( + this, + function () {myself.showMessage('saved.', 2); }, + this.githubError() + ); + } +}; + IDE_Morph.prototype.exportProjectMedia = function (name) { var menu, media; this.serializer.isCollectingMedia = true; @@ -3811,6 +4130,42 @@ IDE_Morph.prototype.cloudError = function () { }; }; +IDE_Morph.prototype.githubError = function () { + var myself = this; + + function getURL(url) { + try { + var request = new XMLHttpRequest(); + request.open('GET', url, false); + request.send(); + if (request.status === 200) { + return request.responseText; + } + return null; + } catch (err) { + return null; + } + } + + return function (responseText, url) { + var response = responseText; + if (myself.shield) { + myself.shield.destroy(); + myself.shield = null; + } + if (response.length > 50) { + response = response.substring(0, 50) + '...'; + } + new DialogBoxMorph().inform( + 'GitHub', + (url ? url + '\n' : '') + + response, + myself.world(), + myself.cloudIcon(null, new Color(180, 0, 0)) + ); + }; +}; + IDE_Morph.prototype.cloudIcon = function (height, color) { var clr = color || DialogBoxMorph.prototype.titleBarColor, isFlat = MorphicPreferences.isFlat, @@ -3827,6 +4182,31 @@ IDE_Morph.prototype.cloudIcon = function (height, color) { return icon; }; +IDE_Morph.prototype.setCloudURL = function () { + new DialogBoxMorph( + null, + function (url) { + SnapCloud.url = url; + } + ).withKey('cloudURL').prompt( + 'Cloud URL', + SnapCloud.url, + this.world(), + null, + { + 'Snap!Cloud' : + 'https://snapcloud.miosoft.com/miocon/app/' + + 'login?_app=SnapCloud', + 'local network lab' : + '192.168.2.107:8087/miocon/app/login?_app=SnapCloud', + 'local network office' : + '192.168.186.146:8087/miocon/app/login?_app=SnapCloud', + 'localhost dev' : + 'localhost/miocon/app/login?_app=SnapCloud' + } + ); +}; + // IDE_Morph synchronous Http data fetching IDE_Morph.prototype.getURL = function (url) { @@ -3923,7 +4303,7 @@ ProjectDialogMorph.prototype.init = function (ide, task) { // additional properties: this.ide = ide; this.task = task || 'open'; // String describing what do do (open, save) - this.source = ide.source || 'local'; // or 'cloud' or 'examples' + this.source = ide.source || 'local'; // 'or 'cloud' or 'github' or 'examples' this.projectList = []; // [{name: , thumb: , notes:}] this.handle = null; @@ -3934,6 +4314,8 @@ ProjectDialogMorph.prototype.init = function (ide, task) { this.notesText = null; this.notesField = null; this.deleteButton = null; + this.shareButton = null; + this.unshareButton = null; // initialize inherited properties: ProjectDialogMorph.uber.init.call( @@ -3981,6 +4363,7 @@ ProjectDialogMorph.prototype.buildContents = function () { } this.addSourceButton('cloud', localize('Cloud'), 'cloud'); + this.addSourceButton('github', localize('GitHub'), 'github'); this.addSourceButton('local', localize('Browser'), 'storage'); if (this.task === 'open') { this.addSourceButton('examples', localize('Examples'), 'poster'); @@ -4073,6 +4456,10 @@ ProjectDialogMorph.prototype.buildContents = function () { this.addButton('saveProject', 'Save'); this.action = 'saveProject'; } + this.shareButton = this.addButton('shareProject', 'Share'); + this.unshareButton = this.addButton('unshareProject', 'Unshare'); + this.shareButton.hide(); + this.unshareButton.hide(); this.deleteButton = this.addButton('deleteProject', 'Delete'); this.addButton('cancel', 'Cancel'); @@ -4225,6 +4612,20 @@ ProjectDialogMorph.prototype.setSource = function (source) { } ); return; + case 'github': + msg = myself.ide.showMessage('Updating\nproject list...'); + this.projectList = []; + GitHub.getProjectList( + function (projectList) { + myself.installGitHubProjectList(projectList); + msg.destroy(); + }, + function (err, lbl) { + msg.destroy(); + myself.ide.githubError().call(null, err, lbl); + } + ); + return; case 'examples': this.projectList = this.getExamplesProjectList(); break; @@ -4277,7 +4678,7 @@ ProjectDialogMorph.prototype.setSource = function (source) { } myself.edit(); }; - } else { // 'examples', 'cloud' is initialized elsewhere + } else { // 'examples', 'cloud' and 'github' is initialized elsewhere this.listField.action = function (item) { var src, xml; if (item === undefined) {return; } @@ -4302,6 +4703,8 @@ ProjectDialogMorph.prototype.setSource = function (source) { }; } this.body.add(this.listField); + this.shareButton.hide(); + this.unshareButton.hide(); if (this.source === 'local') { this.deleteButton.show(); } else { // examples @@ -4378,6 +4781,83 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) { function (element) { return element.ProjectName; } : null, + [ // format: display shared project names bold + [ + 'bold', + function (proj) {return proj.Public === 'true'; } + ] + ], + function () {myself.ok(); } + ); + this.fixListFieldItemColors(); + this.listField.fixLayout = nop; + this.listField.edge = InputFieldMorph.prototype.edge; + this.listField.fontSize = InputFieldMorph.prototype.fontSize; + this.listField.typeInPadding = InputFieldMorph.prototype.typeInPadding; + this.listField.contrast = InputFieldMorph.prototype.contrast; + this.listField.drawNew = InputFieldMorph.prototype.drawNew; + this.listField.drawRectBorder = InputFieldMorph.prototype.drawRectBorder; + + this.listField.action = function (item) { + if (item === undefined) {return; } + if (myself.nameField) { + myself.nameField.setContents(item.ProjectName || ''); + } + if (myself.task === 'open') { + myself.notesText.text = item.Notes || ''; + myself.notesText.drawNew(); + myself.notesField.contents.adjustBounds(); + myself.preview.texture = item.Thumbnail || null; + myself.preview.cachedTexture = null; + myself.preview.drawNew(); + (new SpeechBubbleMorph(new TextMorph( + localize('last changed') + '\n' + item.Updated, + null, + null, + null, + null, + 'center' + ))).popUp( + myself.world(), + myself.preview.rightCenter().add(new Point(2, 0)) + ); + } + if (item.Public === 'true') { + myself.shareButton.hide(); + myself.unshareButton.show(); + } else { + myself.unshareButton.hide(); + myself.shareButton.show(); + } + myself.buttons.fixLayout(); + myself.fixLayout(); + myself.edit(); + }; + this.body.add(this.listField); + this.shareButton.show(); + this.unshareButton.hide(); + this.deleteButton.show(); + this.buttons.fixLayout(); + this.fixLayout(); + if (this.task === 'open') { + this.clearDetails(); + } +}; + +ProjectDialogMorph.prototype.installGitHubProjectList = function (pl) { + var myself = this; + this.projectList = pl || []; + this.projectList.sort(function (x, y) { + return x.ProjectName < y.ProjectName ? -1 : 1; + }); + + this.listField.destroy(); + this.listField = new ListMorph( + this.projectList, + this.projectList.length > 0 ? + function (element) { + return element.ProjectName; + } : null, [], function () {myself.ok(); } ); @@ -4443,6 +4923,8 @@ ProjectDialogMorph.prototype.openProject = function () { this.ide.source = this.source; if (this.source === 'cloud') { this.openCloudProject(proj); + } else if (this.source === 'github') { + this.openGitHubProject(proj); } else if (this.source === 'examples') { src = this.ide.getURL( 'http://snap.berkeley.edu/snapsource/Examples/' + @@ -4468,20 +4950,58 @@ ProjectDialogMorph.prototype.openCloudProject = function (project) { ]); }; +ProjectDialogMorph.prototype.openGitHubProject = function (project) { + var myself = this; + myself.ide.nextSteps([ + function () { + myself.ide.showMessage('Fetching project\nfrom GitHub...'); + }, + function () { + myself.rawOpenGitHubProject(project); + } + ]); +}; + ProjectDialogMorph.prototype.rawOpenCloudProject = function (proj) { var myself = this; - SnapCloud.getProject( - {'projectname': proj.ProjectName, 'username': SnapCloud.username}, + SnapCloud.reconnect( + function () { + SnapCloud.callService( + 'getProject', + function (response) { + SnapCloud.disconnect(); + myself.ide.source = 'cloud'; + myself.ide.droppedText(response[0].SourceCode); + if (proj.Public === 'true') { + location.hash = '#present:Username=' + + encodeURIComponent(SnapCloud.username) + + '&ProjectName=' + + encodeURIComponent(proj.ProjectName); + } + }, + myself.ide.cloudError(), + [proj.ProjectName] + ); + }, + myself.ide.cloudError() + ); + this.destroy(); +}; + +ProjectDialogMorph.prototype.rawOpenGitHubProject = function (proj) { + var myself = this; + GitHub.getProject( + GitHub.username, + proj.ProjectName, function (code, media) { - myself.ide.source = 'cloud'; + myself.ide.source = 'github'; myself.ide.droppedText(media); myself.ide.droppedText(code); }, - myself.ide.cloudError() + myself.ide.githubError() ); this.destroy(); }; - ProjectDialogMorph.prototype.saveProject = function () { var name = this.nameField.contents().text.text, notes = this.notesText.text, @@ -4508,6 +5028,25 @@ ProjectDialogMorph.prototype.saveProject = function () { this.ide.setProjectName(name); myself.saveCloudProject(); } + } else if (this.source === 'github') { + if (detect( + this.projectList, + function (item) {return item.ProjectName === name; } + )) { + this.ide.confirm( + localize( + 'Are you sure you want to replace' + ) + '\n"' + name + '"?', + 'Replace Project', + function () { + myself.ide.setProjectName(name); + myself.saveGitHubProject(); + } + ); + } else { + this.ide.setProjectName(name); + myself.saveGitHubProject(); + } } else { // 'local' if (detect( this.projectList, @@ -4549,6 +5088,156 @@ ProjectDialogMorph.prototype.saveCloudProject = function () { this.destroy(); }; +ProjectDialogMorph.prototype.saveGitHubProject = function () { + var myself = this; + this.ide.showMessage('Committing project\nto GitHub...'); + GitHub.saveProject( + this.ide, + function () { + myself.ide.source = 'github'; + myself.ide.showMessage('saved.', 2); + }, + this.ide.githubError() + ); + this.destroy(); +}; + +ProjectDialogMorph.prototype.deleteProject = function () { + var myself = this, + proj, + idx, + name; + + if (this.source === 'github') { + // TODO: not implemented + } else if (this.source === 'cloud') { + proj = this.listField.selected; + if (proj) { + this.ide.confirm( + localize( 'Are you sure you want to delete' + ) + '\n"' + proj.ProjectName + '"?', + 'Delete Project', + function () { + SnapCloud.reconnect( + 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.cloudError() + ); + } + ); + } + } else { // 'local, examples' + if (this.listField.selected) { + name = this.listField.selected.name; + this.ide.confirm( + localize( + 'Are you sure you want to delete' + ) + '\n"' + name + '"?', + 'Delete Project', + function () { + delete localStorage['-snap-project-' + name]; + myself.setSource(myself.source); // refresh list + } + ); + } + } +}; + +ProjectDialogMorph.prototype.shareProject = function () { + var myself = this, + proj = this.listField.selected, + entry = this.listField.active; + + if (proj) { + this.ide.confirm( + localize( + 'Are you sure you want to publish' + ) + '\n"' + proj.ProjectName + '"?', + 'Share Project', + function () { + myself.ide.showMessage('sharing\nproject...'); + SnapCloud.reconnect( + 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] + ); + }, + myself.ide.cloudError() + ); + } + ); + } +}; + +ProjectDialogMorph.prototype.unshareProject = function () { + var myself = this, + 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', + function () { + myself.ide.showMessage('unsharing\nproject...'); + SnapCloud.reconnect( + 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] + ); + }, + myself.ide.cloudError() + ); + } + ); + } +}; + ProjectDialogMorph.prototype.edit = function () { if (this.nameField) { this.nameField.edit(); @@ -16,9 +16,10 @@ <script type="text/javascript" src="xml.js"></script> <script type="text/javascript" src="store.js"></script> <script type="text/javascript" src="locale.js"></script> + <script type="text/javascript" src="cloud.js"></script> <script type="text/javascript" src="octokit/promise-1.0.0.js"></script> <script type="text/javascript" src="octokit/octokit.js"></script> - <script type="text/javascript" src="cloud.js"></script> + <script type="text/javascript" src="github.js"></script> <script type="text/javascript" src="sha512.js"></script> <script type="text/javascript"> var world; |
