2017-09-28 16:30:13 +00:00
|
|
|
/*
|
|
|
|
|
|
|
|
cloud.js
|
|
|
|
|
|
|
|
a backend API for SNAP!
|
|
|
|
|
|
|
|
written by Bernat Romagosa
|
2017-10-02 16:43:50 +00:00
|
|
|
inspired in the old cloud API by Jens Mönig
|
2017-09-28 16:30:13 +00:00
|
|
|
|
|
|
|
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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
};
|
|
|
|
|
2017-10-04 09:46:50 +00:00
|
|
|
// 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);
|
|
|
|
};
|
|
|
|
|
2017-09-28 16:30:13 +00:00
|
|
|
|
2017-10-04 15:59:10 +00:00
|
|
|
// Low level functionality
|
2017-10-02 16:43:50 +00:00
|
|
|
|
2017-10-04 09:46:50 +00:00
|
|
|
Cloud.prototype.request = function (
|
|
|
|
method,
|
|
|
|
path,
|
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
errorMsg,
|
|
|
|
wantsRawResponse,
|
|
|
|
body) {
|
|
|
|
|
2017-09-28 16:30:13 +00:00
|
|
|
var request = new XMLHttpRequest(),
|
|
|
|
myself = this;
|
2017-10-04 09:46:50 +00:00
|
|
|
|
2017-09-28 16:30:13 +00:00
|
|
|
try {
|
|
|
|
request.open(
|
2017-10-04 09:46:50 +00:00
|
|
|
method,
|
2017-09-28 16:30:13 +00:00
|
|
|
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) {
|
2017-10-04 15:59:10 +00:00
|
|
|
var response =
|
|
|
|
(!wantsRawResponse ||
|
|
|
|
(request.responseText.indexOf('{"errors"') === 0)) ?
|
|
|
|
JSON.parse(request.responseText) :
|
|
|
|
request.responseText;
|
|
|
|
|
2017-09-28 16:30:13 +00:00
|
|
|
if (response.errors) {
|
|
|
|
onError.call(
|
|
|
|
null,
|
|
|
|
response.errors[0],
|
|
|
|
errorMsg
|
|
|
|
);
|
|
|
|
} else {
|
2017-10-04 09:46:50 +00:00
|
|
|
if (onSuccess) {
|
|
|
|
onSuccess.call(null, response.message || response);
|
|
|
|
}
|
2017-09-28 16:30:13 +00:00
|
|
|
}
|
|
|
|
} else {
|
2017-10-04 09:46:50 +00:00
|
|
|
if (onError) {
|
2017-09-28 16:30:13 +00:00
|
|
|
onError.call(
|
2017-10-04 09:46:50 +00:00
|
|
|
null,
|
2017-10-25 14:59:58 +00:00
|
|
|
errorMsg || Cloud.genericErrorMessage,
|
|
|
|
myself.url
|
2017-09-28 16:30:13 +00:00
|
|
|
);
|
|
|
|
} else {
|
2017-10-04 09:46:50 +00:00
|
|
|
myself.genericError();
|
2017-09-28 16:30:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
request.send(body);
|
|
|
|
} catch (err) {
|
|
|
|
onError.call(this, err.toString(), 'Cloud Error');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-10-04 09:46:50 +00:00
|
|
|
Cloud.prototype.withCredentialsRequest = function (
|
|
|
|
method,
|
|
|
|
path,
|
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
errorMsg,
|
2017-10-04 12:22:15 +00:00
|
|
|
wantsRawResponse,
|
|
|
|
body) {
|
2017-10-04 09:46:50 +00:00
|
|
|
|
|
|
|
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,
|
2017-10-04 12:22:15 +00:00
|
|
|
wantsRawResponse,
|
|
|
|
body);
|
2017-10-04 09:46:50 +00:00
|
|
|
} else {
|
|
|
|
onError.call(this, 'You are not logged in', 'Snap!Cloud');
|
2017-10-02 16:43:50 +00:00
|
|
|
}
|
2017-10-04 09:46:50 +00:00
|
|
|
}
|
|
|
|
);
|
2017-10-02 16:43:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2017-09-28 16:30:13 +00:00
|
|
|
// Credentials management
|
|
|
|
|
2017-10-04 17:53:25 +00:00
|
|
|
Cloud.prototype.initSession = function (onSuccess) {
|
|
|
|
var myself = this;
|
|
|
|
this.request(
|
|
|
|
'POST',
|
|
|
|
'/init',
|
|
|
|
function () { myself.checkCredentials(onSuccess); },
|
|
|
|
nop,
|
|
|
|
null,
|
|
|
|
true
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2017-09-28 16:30:13 +00:00
|
|
|
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); }
|
|
|
|
},
|
2017-10-02 16:43:50 +00:00
|
|
|
onError
|
|
|
|
);
|
2017-09-28 16:30:13 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Cloud.prototype.getCurrentUser = function (onSuccess, onError) {
|
2017-10-04 09:46:50 +00:00
|
|
|
this.request('GET', '/users/c', onSuccess, onError, 'Could not retrieve current user');
|
2017-09-28 16:30:13 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Cloud.prototype.logout = function (onSuccess, onError) {
|
2017-10-04 17:53:25 +00:00
|
|
|
this.username = null;
|
2017-10-04 09:46:50 +00:00
|
|
|
this.request(
|
|
|
|
'POST',
|
2017-10-04 17:53:25 +00:00
|
|
|
'/logout',
|
2017-09-28 16:30:13 +00:00
|
|
|
onSuccess,
|
|
|
|
onError,
|
2017-10-02 16:43:50 +00:00
|
|
|
'logout failed'
|
|
|
|
);
|
2017-09-28 16:30:13 +00:00
|
|
|
};
|
|
|
|
|
2017-10-04 17:53:25 +00:00
|
|
|
Cloud.prototype.login = function (username, password, persist, onSuccess, onError) {
|
2017-09-28 16:30:13 +00:00
|
|
|
var myself = this;
|
2017-10-04 09:46:50 +00:00
|
|
|
this.request(
|
|
|
|
'POST',
|
2017-10-04 17:53:25 +00:00
|
|
|
'/users/' + username + '/login?' +
|
|
|
|
this.encodeDict({
|
|
|
|
password: password,
|
|
|
|
persist: persist
|
|
|
|
}),
|
2017-09-28 16:30:13 +00:00
|
|
|
function () {
|
|
|
|
myself.checkCredentials(onSuccess, onError);
|
|
|
|
},
|
|
|
|
onError,
|
|
|
|
'login failed');
|
|
|
|
};
|
|
|
|
|
|
|
|
Cloud.prototype.signup = function (username, password, password_repeat, email, onSuccess, onError){
|
2017-10-04 09:46:50 +00:00
|
|
|
this.request(
|
|
|
|
'POST',
|
2017-09-28 16:30:13 +00:00
|
|
|
'/users/' + username + '?' + this.encodeDict({
|
|
|
|
email: email,
|
|
|
|
password: password,
|
|
|
|
password_repeat: password_repeat
|
|
|
|
}),
|
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
'signup failed');
|
|
|
|
};
|
|
|
|
|
|
|
|
// Projects
|
|
|
|
|
|
|
|
Cloud.prototype.saveProject = function (ide, onSuccess, onError) {
|
2017-10-02 16:43:50 +00:00
|
|
|
var myself = this;
|
2017-09-28 16:30:13 +00:00
|
|
|
this.checkCredentials(
|
|
|
|
function (username) {
|
|
|
|
if (username) {
|
2017-10-02 16:43:50 +00:00
|
|
|
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');
|
|
|
|
}
|
|
|
|
|
2017-09-28 16:30:13 +00:00
|
|
|
// check if serialized data can be parsed back again
|
|
|
|
try {
|
2017-10-02 16:43:50 +00:00
|
|
|
ide.serializer.parse(body.xml);
|
2017-09-28 16:30:13 +00:00
|
|
|
} catch (err) {
|
|
|
|
ide.showMessage('Serialization of program data failed:\n' + err);
|
|
|
|
throw new Error('Serialization of program data failed:\n' + err);
|
|
|
|
}
|
2017-10-02 16:43:50 +00:00
|
|
|
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();
|
2017-09-28 16:30:13 +00:00
|
|
|
|
2017-10-02 16:43:50 +00:00
|
|
|
ide.showMessage('Uploading ' + Math.round(size / 1024) + ' KB...');
|
2017-09-28 16:30:13 +00:00
|
|
|
|
2017-10-04 09:46:50 +00:00
|
|
|
myself.request(
|
|
|
|
'POST',
|
|
|
|
'/projects/' + username + '/' + ide.projectName,
|
2017-10-02 16:43:50 +00:00
|
|
|
onSuccess,
|
|
|
|
onError,
|
2017-10-04 09:46:50 +00:00
|
|
|
'Project could not be saved',
|
|
|
|
false,
|
|
|
|
JSON.stringify(body), // POST body
|
2017-10-02 16:43:50 +00:00
|
|
|
);
|
2017-09-28 16:30:13 +00:00
|
|
|
} else {
|
|
|
|
onError.call(this, 'You are not logged in', 'Snap!Cloud');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2017-10-24 10:58:37 +00:00
|
|
|
Cloud.prototype.getProjectList = function (onSuccess, onError, withThumbnail) {
|
|
|
|
var path = '/projects/%username';
|
|
|
|
|
|
|
|
if (withThumbnail) {
|
|
|
|
path += '?withthumbnail=true';
|
|
|
|
}
|
|
|
|
|
2017-10-04 09:46:50 +00:00
|
|
|
this.withCredentialsRequest(
|
|
|
|
'GET',
|
2017-10-24 10:58:37 +00:00
|
|
|
path,
|
2017-10-04 09:46:50 +00:00
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
'Could not fetch projects'
|
2017-10-02 16:43:50 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2017-10-24 10:58:37 +00:00
|
|
|
Cloud.prototype.getPublishedProjectList = function (username, page, pageSize, searchTerm, onSuccess, onError, withThumbnail) {
|
2017-10-13 16:29:47 +00:00
|
|
|
var path = '/projects' + (username ? '/' + username : '') + '?ispublished=true';
|
|
|
|
|
2017-10-24 10:58:37 +00:00
|
|
|
if (withThumbnail) {
|
|
|
|
path += '&withthumbnail=true';
|
|
|
|
}
|
|
|
|
|
2017-10-13 16:29:47 +00:00
|
|
|
if (page) {
|
|
|
|
path += '&page=' + page + '&pagesize=' + (pageSize || 16);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (searchTerm) {
|
|
|
|
path += '&matchtext=' + searchTerm;
|
|
|
|
}
|
|
|
|
|
2017-10-10 11:00:49 +00:00
|
|
|
this.request(
|
|
|
|
'GET',
|
2017-10-13 16:29:47 +00:00
|
|
|
path,
|
2017-10-10 11:00:49 +00:00
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
'Could not fetch projects'
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2017-10-13 16:29:47 +00:00
|
|
|
Cloud.prototype.getThumbnail = function (username, projectName, onSuccess, onError) {
|
|
|
|
if (username) {
|
|
|
|
this.request(
|
|
|
|
'GET',
|
|
|
|
'/projects/' + username + '/' + projectName + '/thumbnail',
|
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
'Could not fetch thumbnail',
|
|
|
|
true
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
this.withCredentialsRequest(
|
|
|
|
'GET',
|
|
|
|
'/projects/%username/' + projectName + '/thumbnail',
|
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
'Could not fetch thumbnail',
|
|
|
|
true
|
|
|
|
);
|
|
|
|
}
|
2017-10-02 16:43:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Cloud.prototype.getRawProject = function (projectName, onSuccess, onError) {
|
2017-10-04 09:46:50 +00:00
|
|
|
this.withCredentialsRequest(
|
|
|
|
'GET',
|
|
|
|
'/projects/%username/' + projectName,
|
|
|
|
onSuccess,
|
|
|
|
onError,
|
2017-10-04 15:59:10 +00:00
|
|
|
'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,
|
2017-10-04 09:46:50 +00:00
|
|
|
true
|
2017-10-02 16:43:50 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2017-10-09 10:37:34 +00:00
|
|
|
Cloud.prototype.getProjectMetadata = function (projectName, username, onSuccess, onError) {
|
|
|
|
this.request(
|
|
|
|
'GET',
|
|
|
|
'/projects/' + username + '/' + projectName + '/metadata',
|
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
'Could not fetch metadata for ' + projectName
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2017-10-02 16:43:50 +00:00
|
|
|
Cloud.prototype.deleteProject = function (projectName, onSuccess, onError) {
|
2017-10-04 09:46:50 +00:00
|
|
|
this.withCredentialsRequest(
|
|
|
|
'DELETE',
|
|
|
|
'/projects/%username/' + projectName,
|
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
'Could not delete project'
|
2017-10-02 16:43:50 +00:00
|
|
|
);
|
2017-09-28 16:30:13 +00:00
|
|
|
};
|
|
|
|
|
2017-10-04 12:22:15 +00:00
|
|
|
Cloud.prototype.shareProject = function (projectName, onSuccess, onError) {
|
|
|
|
this.withCredentialsRequest(
|
|
|
|
'POST',
|
|
|
|
'/projects/%username/' + projectName + '/metadata?ispublic=true',
|
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
'Could not share project'
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
Cloud.prototype.unshareProject = function (projectName, onSuccess, onError) {
|
|
|
|
this.withCredentialsRequest(
|
|
|
|
'POST',
|
|
|
|
'/projects/%username/' + projectName + '/metadata?ispublic=false&ispublished=false',
|
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
'Could not unshare project'
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
Cloud.prototype.publishProject = function (projectName, onSuccess, onError) {
|
|
|
|
this.withCredentialsRequest(
|
|
|
|
'POST',
|
|
|
|
'/projects/%username/' + projectName + '/metadata?ispublished=true',
|
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
'Could not publish project'
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
Cloud.prototype.unpublishProject = function (projectName, onSuccess, onError) {
|
|
|
|
this.withCredentialsRequest(
|
|
|
|
'POST',
|
|
|
|
'/projects/%username/' + projectName + '/metadata?ispublished=false',
|
|
|
|
onSuccess,
|
|
|
|
onError,
|
|
|
|
'Could not unpublish project'
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2017-10-25 14:59:58 +00:00
|
|
|
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 })
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2017-09-28 16:30:13 +00:00
|
|
|
var SnapCloud = new Cloud('http://localhost:8080');
|