// Beetle Blocks cloud // Inspired in Snap! cloud function BeetleCloud (url, ide) { this.init(url, ide); }; BeetleCloud.prototype.init = function (url, ide) { this.url = url; this.ide = ide; this.checkCredentials(); }; BeetleCloud.prototype.parseDict = Cloud.prototype.parseDict; BeetleCloud.prototype.encodeDict = Cloud.prototype.encodeDict; BeetleCloud.prototype.clear = function () { this.username = null; }; BeetleCloud.prototype.get = function (path, callBack, errorCall, errorMsg) { var request = new XMLHttpRequest(), myself = this; try { request.open( 'GET', 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) { if(request.status === 404) { if (errorCall) errorCall.call( null, myself.url, errorMsg ); else console.log("error in checking credentials") return false; } var response = JSON.parse(request.responseText); if (!response.error) { callBack.call(null, response); } else { errorCall.call( null, response.error, errorMsg ); } } else { if (typeof errorCall != 'undefined') { errorCall.call( null, myself.url, errorMsg ); } } } }; request.send(); } catch (err) { errorCall.call(this, err.toString(), 'TurtleCloud'); } }; BeetleCloud.prototype.getImage = function (path, callBack, errorCall, errorMsg) { var request = new XMLHttpRequest(), myself = this; try { request.open( 'GET', 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) { if(request.status === 404) { if (errorCall) errorCall.call( null, myself.url, errorMsg ); else console.log("error in checking credentials") return false; } callBack.call(null, request.responseText); } else { if (typeof errorCall != 'undefined') { errorCall.call( null, myself.url, errorMsg ); } } } }; request.send(); } catch (err) { errorCall.call(this, err.toString(), 'TurtleCloud'); } }; BeetleCloud.prototype.post = function (path, body, callBack, errorCall, errorMsg) { var request = new XMLHttpRequest(), myself = this; try { request.open( 'POST', this.url + path, true ); request.setRequestHeader( 'Content-Type', 'application/json' ); request.withCredentials = true; request.onreadystatechange = function () { if (request.readyState === 4) { if (request.responseText) { var response = JSON.parse(request.responseText); if (response.error) { errorCall.call( this, response.error, 'TurtleCloud' ); } else { callBack.call( null, response.text, 'TurtleCloud' ); } } else { if (typeof errorCall !== undefined) { errorCall.call( null, myself.url + path, localize('could not connect to:') ); } } } }; request.send(body); } catch (err) { errorCall.call(this, err.toString(), 'TurtleCloud'); } }; BeetleCloud.prototype.getCurrentUser = function (callback, errorCallback) { this.get('/user', callback, errorCallback, 'Could not retrieve current user'); }; BeetleCloud.prototype.checkCredentials = function (callback, errorCallback) { var myself = this; this.getCurrentUser( function (user) { if (user.username) { myself.username = user.username; } if (callback) { callback.call(null, user); } }, errorCallback); }; BeetleCloud.prototype.logout = function (callBack, errorCall) { this.get('/users/logout', callBack, errorCall, 'logout failed'); }; BeetleCloud.prototype.shareProject = function (shareOrNot, projectName, callBack, errorCall) { var myself = this; this.checkCredentials( function (user) { if (user.username) { myself.get('/users/' + encodeURIComponent(user.username) + '/projects/' + encodeURIComponent(projectName) + '/visibility?ispublic=' + shareOrNot, // path callBack, // ok callback errorCall, // error callback (shareOrNot ? 'S' : 'Uns') + 'haring failed'); // error message } else { errorCall.call(this, 'You are not logged in', 'TurtleCloud'); } }, errorCall ); }; BeetleCloud.prototype.saveProject = function (ignorethis, discardthis, callBack, errorCall) { var myself = this; this.ide.stage.reRender(); this.checkCredentials( function (user) { if (user.username) { project = new Project(myself.ide.scenes, myself.ide.scene); project.name = myself.ide.projectName; project.notes = myself.ide.projectNotes; project.origName = myself.ide.origName; project.origCreator = myself.ide.origCreator; project.creator = myself.ide.creator; project.remixHistory = myself.ide.remixHistory; var pdata = myself.ide.serializer.serialize(project); // check if serialized data can be parsed back again try { myself.ide.serializer.parse(pdata); } catch (err) { myself.ide.showMessage('Serialization of program data failed:\n' + err); throw new Error('Serialization of program data failed:\n' + err); } myself.ide.showMessage('Uploading project...'); //(path, body, callBack, errorCall, errorMsg) myself.post( '/projects/save?projectname=' + encodeURIComponent(myself.ide.projectName) + '&username=' + encodeURIComponent(myself.username) + '&tags=' + encodeURIComponent(myself.ide.tags) + '&ispublic=true', // path pdata, // body callBack, errorCall, 'Project could not be saved' // error message ) } else { errorCall.call(this, 'You are not logged in', 'TurtleCloud'); return; } } ); }; // Backwards compatibility with old cloud, to be removed BeetleCloud.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 takes two args var parsedId = id.split('&').map(function(each){return each.split('=')[1]}), username = decodeURIComponent(parsedId[0]), projectName = decodeURIComponent(parsedId[1]); this.fetchProject(projectName, callBack, errorCall, username); }; BeetleCloud.prototype.fetchProject = function (projectName, callBack, errorCall, publicUsername) { var myself = this; this.checkCredentials( function (user) { var username = publicUsername || user.username; if (!username) { errorCall.call(this, 'Project could not be fetched', 'TurtleCloud'); return; } else { myself.get( '/users/' + encodeURIComponent(username) + '/projects/' + encodeURIComponent(projectName), function (response) { callBack.call(null, response.contents); }, errorCall, 'Could not fetch project' ) } }, errorCall ); }; BeetleCloud.prototype.deleteProject = function (projectName, callBack, errorCall) { var myself = this; this.checkCredentials( function (user) { if (!user.username) { errorCall.call(this, 'You are not logged in', 'TurtleCloud'); return; } else { myself.get( '/users/' + encodeURIComponent(user.username) + '/projects/' + encodeURIComponent(projectName) + '/delete', function (response) { callBack.call(null, response.text); }, errorCall, 'Could not delete project' ); } }, errorCall ); } BeetleCloud.prototype.getProjectList = function (callBack, errorCall) { var myself = this; this.checkCredentials( function (user) { if (!user.username) { errorCall.call(this, 'You are not logged in', 'TurtleCloud'); return; } else { myself.get( '/users/' + encodeURIComponent(myself.username) + '/projects', function (response) { if (Object.keys(response).length > 0) { response.forEach(function(eachProject) { // This looks absurd, but PostgreSQL doesn't respect case eachProject.Public = eachProject.ispublic ? 'true' : 'false'; // compatibility with old cloud eachProject.ProjectName = eachProject.projectname; eachProject.Thumbnail = eachProject.thumbnail; eachProject.Updated = eachProject.updated; eachProject.Notes = eachProject.notes; }); callBack.call(null, response); } else { alert("empty") callBack.call(null, []); } }, errorCall, 'Could not fetch project list' ); } }, errorCall ); }; BeetleCloud.prototype.getThumbnail = function (username, projectName, callBack, errorCall) { this.getImage('/users/' + this.username + '/projects/' + encodeURIComponent(projectName) + '/image', // path callBack, // ok callback () => {}, // error callback 'Could not fetch thumbnail'); // error message }; Cloud = BeetleCloud; var SnapCloud = new BeetleCloud( '/api' ); // Overrides to be moved to the proper corresponding files after this goes live // gui.js // In the BeetleCloud we allow uppercase characters in usernames // so we need to override this function IDE_Morph.prototype.openIn = function (world) { var hash, myself = this, urlLanguage = null; SnapCloud.checkCredentials( function (user) { if (user.username) { myself.source = 'cloud'; } }); this.buildPanes(); world.add(this); world.userMenu = this.userMenu; // override SnapCloud's user message with Morphic SnapCloud.message = function (string) { var m = new MenuMorph(null, string), intervalHandle; m.popUpCenteredInWorld(world); intervalHandle = setInterval(function () { m.destroy(); clearInterval(intervalHandle); }, 2000); }; // prevent non-DialogBoxMorphs from being dropped // onto the World in user-mode world.reactToDropOf = function (morph) { if (!(morph instanceof DialogBoxMorph)) { if (world.hand.grabOrigin) { morph.slideBackTo(world.hand.grabOrigin); } else { world.hand.grab(morph); } } }; this.reactToWorldResize(world.bounds); function getURL(url) { try { var request = new XMLHttpRequest(); request.open('GET', url, false); request.send(); if (request.status === 200) { return request.responseText; } throw new Error('unable to retrieve ' + url); } catch (err) { myself.showMessage('unable to retrieve project'); return ''; } } // This function returns the value of a parameter given its key function getParameterByName(name) { var param = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'), regex = new RegExp('[\\?&]' + param + '=([^&#]*)'), results = regex.exec(location.href); return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); } // dynamic notifications from non-source text files // has some issues, commented out for now /* this.cloudMsg = getURL('http://snap.berkeley.edu/cloudmsg.txt'); motd = getURL('http://snap.berkeley.edu/motd.txt'); if (motd) { this.inform('Snap!', motd); } */ function interpretUrlAnchors() { var dict; if (location.hash.substr(0, 6) === '#open:') { hash = location.hash.substr(6); if (hash.charAt(0) === '%' || hash.search(/\%(?:[0-9a-f]{2})/i) > -1) { hash = decodeURIComponent(hash); } if (contains( ['project', 'blocks', 'sprites', 'snapdata'].map( function (each) { return hash.substr(0, 8).indexOf(each); } ), 1 )) { this.droppedText(hash); } else { this.droppedText(getURL(hash)); } } else if (location.hash.substr(0, 5) === '#run:') { hash = location.hash.substr(5); if (hash.charAt(0) === '%' || hash.search(/\%(?:[0-9a-f]{2})/i) > -1) { hash = decodeURIComponent(hash); } if (hash.substr(0, 8) === '') { this.rawOpenProjectString(hash); } else { this.rawOpenProjectString(getURL(hash)); } this.toggleAppMode(true); this.runScripts(); } else if (location.hash.substr(0, 9) === '#present:') { 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 the cloud...'); dict = SnapCloud.parseDict(location.hash.substr(9)); SnapCloud.getPublicProject( SnapCloud.encodeDict(dict), function (projectData) { var msg; myself.nextSteps([ function () { msg = myself.showMessage('Opening project...'); }, function () {nop(); }, // yield (bug in Chrome) function () { if (projectData.indexOf(' 0 ? function (element) { return element.name; } : null, null, 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.drawRectBorder = InputFieldMorph.prototype.drawRectBorder; if (this.source === 'local') { this.listField.action = function (item) { var src, xml; if (item === undefined) {return; } if (myself.nameField) { myself.nameField.setContents(item.name || ''); } if (myself.task === 'open') { src = localStorage['-snap-project-' + item.name]; xml = myself.ide.serializer.parse(src); myself.notesText.text = xml.childNamed('notes').contents || ''; myself.notesText.fixLayout(); myself.notesField.contents.adjustBounds(); myself.preview.texture = xml.childNamed('thumbnail').contents || null; myself.preview.cachedTexture = null; myself.preview.fixLayout(); } myself.edit(); }; } else { // 'examples', 'cloud' is initialized elsewhere this.listField.action = function (item) { var src, xml; if (item === undefined) {return; } if (myself.nameField) { myself.nameField.setContents(item.name || ''); } //src = myself.ide.getURL(item.path); src = myself.ide.getURL( myself.ide.resourceURL('Examples', item.fileName) ); xml = myself.ide.serializer.parse(src); myself.notesText.text = xml.childNamed('notes').contents || ''; myself.notesText.fixLayout(); myself.notesField.contents.adjustBounds(); myself.preview.texture = xml.childNamed('thumbnail').contents || null; myself.preview.cachedTexture = null; myself.preview.fixLayout(); myself.edit(); }; } this.body.add(this.listField); this.shareButton.hide(); this.unshareButton.hide(); if (this.source === 'local') { this.deleteButton.show(); } else { // examples this.deleteButton.hide(); } this.buttons.fixLayout(); this.fixLayout(); if (this.task === 'open') { this.clearDetails(); } };