turtlestitch/cloud.js

746 wiersze
19 KiB
JavaScript
Czysty Zwykły widok Historia

2017-09-28 16:30:13 +00:00
/*
cloud.js
a backend API for SNAP!
written by Bernat Romagosa
2018-02-08 08:36:16 +00:00
inspired by the original cloud API by Jens Mönig
2017-09-28 16:30:13 +00:00
2018-02-08 08:36:16 +00:00
Copyright (C) 2018 by Bernat Romagosa
2017-09-28 16:30:13 +00:00
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, SnapSerializer, nop, hex_sha512, DialogBoxMorph, Color,
normalizeCanvas*/
2017-09-28 16:30:13 +00:00
2018-06-15 10:48:55 +00:00
modules.cloud = '2018-June-15';
2017-09-28 16:30:13 +00:00
// Global stuff
var Cloud;
// Cloud /////////////////////////////////////////////////////////////
function Cloud() {
this.init();
2018-02-08 08:36:16 +00:00
}
2017-09-28 16:30:13 +00:00
Cloud.prototype.init = function () {
this.url = this.determineCloudDomain();
2017-09-28 16:30:13 +00:00
this.username = null;
};
Cloud.prototype.knownDomains = {
'Snap!Cloud' : 'https://cloud.snap.berkeley.edu',
'Snap!Cloud (cs10)' : 'https://snap-cloud.cs10.org',
'Snap!Cloud (staging)': 'https://snap-staging.cs10.org',
'localhost': 'http://localhost:8080',
'localhost (secure)': 'https://localhost:4431'
};
Cloud.prototype.defaultDomain = Cloud.prototype.knownDomains['Snap!Cloud'];
Cloud.prototype.determineCloudDomain = function () {
// We dynamically determine the domain of the cloud server.
// Thise allows for easy mirrors and development servers.
// The domain is determined by:
// 1. <meta name='snap-cloud-domain' domain="X"> in snap.html.
// 2. The current page's domain
var currentDomain = window.location.host, // host includes the port.
metaTag = document.head.querySelector("[name='snap-cloud-domain']"),
cloudDomain = this.defaultDomain;
if (metaTag) { return metaTag.getAttribute('location'); }
Object.values(this.knownDomains).some(function (server) {
if (Cloud.isMatchingDomain(currentDomain, server)) {
cloudDomain = server;
return true;
}
return false;
});
return cloudDomain;
2018-06-15 10:48:55 +00:00
};
Cloud.isMatchingDomain = function (client, server) {
// A matching domain means that the client-server are not subject to
// 3rd party cookie restrictions.
// see https://tools.ietf.org/html/rfc6265#section-5.1.3
// This matches a domain at end of a subdomain URL.
var position = server.indexOf(client);
switch (position) {
case -1:
return false;
case 0:
return client === server;
default:
return /[\.\/]/.test(server[position - 1]) &&
server.length === position + client.length;
}
2018-06-15 10:48:55 +00:00
};
2017-09-28 16:30:13 +00:00
// 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);
};
// Low level functionality
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) {
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,
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
2018-02-13 15:50:06 +00:00
path.replace('%username', encodeURIComponent(username)),
2017-10-04 09:46:50 +00:00
onSuccess,
onError,
errorMsg,
wantsRawResponse,
body);
2017-10-04 09:46:50 +00:00
} else {
onError.call(this, 'You are not logged in', 'Snap!Cloud');
}
2017-10-04 09:46: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
);
};
Cloud.prototype.checkCredentials = function (onSuccess, onError, response) {
2017-09-28 16:30:13 +00:00
var myself = this;
this.getCurrentUser(
function (user) {
if (user.username) {
myself.username = user.username;
myself.verified = user.verified;
2017-09-28 16:30:13 +00:00
}
2018-02-08 08:36:16 +00:00
if (onSuccess) {
onSuccess.call(
null,
user.username,
user.isadmin,
2018-02-13 11:17:43 +00:00
response ? JSON.parse(response) : null
);
2018-02-08 08:36:16 +00:00
}
2017-09-28 16:30:13 +00:00
},
onError
);
2017-09-28 16:30:13 +00:00
};
Cloud.prototype.getCurrentUser = function (onSuccess, onError) {
2018-02-08 08:36:16 +00:00
this.request(
'GET',
'/users/c',
onSuccess,
onError,
'Could not retrieve current user'
);
2017-09-28 16:30:13 +00:00
};
2017-11-03 15:19:25 +00:00
Cloud.prototype.getUser = function (username, onSuccess, onError) {
2018-02-08 08:36:16 +00:00
this.request(
'GET',
2018-02-13 15:50:06 +00:00
'/users/' + encodeURIComponent(username),
2018-02-08 08:36:16 +00:00
onSuccess,
onError,
'Could not retrieve user'
);
2017-11-03 15:19:25 +00:00
};
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,
'logout failed'
);
2017-09-28 16:30:13 +00:00
};
2018-02-08 08:36:16 +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',
2018-02-13 15:50:06 +00:00
'/users/' + encodeURIComponent(username) + '/login?' +
2017-10-04 17:53:25 +00:00
this.encodeDict({
persist: persist
}),
function (response) {
myself.checkCredentials(onSuccess, onError, response);
2017-09-28 16:30:13 +00:00
},
onError,
'login failed',
'false', // wants raw response
hex_sha512(password) // password travels inside the body
);
2017-09-28 16:30:13 +00:00
};
2018-02-08 08:36:16 +00:00
Cloud.prototype.signup = function (
username,
password,
passwordRepeat,
email,
onSuccess,
onError
) {
2017-10-04 09:46:50 +00:00
this.request(
'POST',
2018-02-13 15:50:06 +00:00
'/users/' + encodeURIComponent(username) + '?' + this.encodeDict({
2017-09-28 16:30:13 +00:00
email: email,
password: hex_sha512(password),
password_repeat: hex_sha512(passwordRepeat)
2017-09-28 16:30:13 +00:00
}),
onSuccess,
onError,
'signup failed');
};
2018-02-08 08:36:16 +00:00
Cloud.prototype.changePassword = function (
password,
newPassword,
passwordRepeat,
onSuccess,
onError
) {
2017-11-10 11:19:21 +00:00
this.withCredentialsRequest(
'POST',
'/users/%username/newpassword?' + this.encodeDict({
oldpassword: hex_sha512(password),
password_repeat: hex_sha512(passwordRepeat),
newpassword: hex_sha512(newPassword)
2017-11-10 11:19:21 +00:00
}),
onSuccess,
onError,
'Could not change password'
);
};
2018-02-09 16:55:10 +00:00
Cloud.prototype.resetPassword = function (username, onSuccess, onError) {
this.request(
'POST',
2018-02-13 15:50:06 +00:00
'/users/' + encodeURIComponent(username) + '/password_reset',
2018-02-09 16:55:10 +00:00
onSuccess,
onError,
'Password reset request failed'
);
};
2018-02-12 15:29:30 +00:00
Cloud.prototype.resendVerification = function (username, onSuccess, onError) {
this.request(
'POST',
2018-02-13 15:50:06 +00:00
'/users/' + encodeURIComponent(username) + '/resendverification',
2018-02-12 15:29:30 +00:00
onSuccess,
onError,
'Could not send verification email'
);
};
2017-09-28 16:30:13 +00:00
// Projects
Cloud.prototype.saveProject = function (ide, onSuccess, onError) {
var myself = this;
2017-09-28 16:30:13 +00:00
this.checkCredentials(
function (username) {
if (username) {
var xml = ide.serializer.serialize(ide.stage),
thumbnail = normalizeCanvas(
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 {
ide.serializer.parse(body.xml);
2017-09-28 16:30:13 +00:00
} catch (err) {
2018-02-08 08:36:16 +00:00
ide.showMessage(
'Serialization of program data failed:\n' + err
);
throw new Error(
'Serialization of program data failed:\n' + err
);
2017-09-28 16:30:13 +00:00
}
if (body.media !== null) {
try {
ide.serializer.parse(body.media);
} catch (err) {
2018-02-08 08:36:16 +00:00
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
2018-02-08 08:36:16 +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/' +
encodeURIComponent(username) +
'/' +
encodeURIComponent(ide.projectName),
onSuccess,
onError,
2017-10-04 09:46:50 +00:00
'Project could not be saved',
false,
2018-02-08 08:36:16 +00:00
JSON.stringify(body) // POST body
);
2017-09-28 16:30:13 +00:00
} else {
onError.call(this, 'You are not logged in', 'Snap!Cloud');
}
}
);
};
Cloud.prototype.getProjectList = function (onSuccess, onError, withThumbnail) {
2018-02-07 08:21:52 +00:00
var path = '/projects/%username?updatingnotes=true';
if (withThumbnail) {
2018-02-07 08:21:52 +00:00
path += '&withthumbnail=true';
}
2017-10-04 09:46:50 +00:00
this.withCredentialsRequest(
'GET',
path,
2017-10-04 09:46:50 +00:00
onSuccess,
onError,
'Could not fetch projects'
);
};
2018-02-08 08:36:16 +00:00
Cloud.prototype.getPublishedProjectList = function (
username,
page,
pageSize,
searchTerm,
onSuccess,
onError,
withThumbnail
) {
var path = '/projects' +
2018-02-13 15:50:06 +00:00
(username ? '/' + encodeURIComponent(username) : '') +
2018-02-08 08:36:16 +00:00
'?ispublished=true';
2017-10-13 16:29:47 +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) {
2018-02-13 15:50:06 +00:00
path += '&matchtext=' + encodeURIComponent(searchTerm);
2017-10-13 16:29:47 +00:00
}
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'
);
};
2018-02-08 08:36:16 +00:00
Cloud.prototype.getThumbnail = function (
username,
projectName,
onSuccess,
onError
) {
2017-11-03 10:30:07 +00:00
this[username ? 'request' : 'withCredentialsRequest'](
'GET',
'/projects/' +
(username ? encodeURIComponent(username) : '%username') +
'/' +
2018-02-13 15:50:06 +00:00
encodeURIComponent(projectName) +
'/thumbnail',
2017-11-03 10:30:07 +00:00
onSuccess,
onError,
'Could not fetch thumbnail',
true
);
};
Cloud.prototype.getProject = function (projectName, delta, onSuccess, onError) {
2017-10-04 09:46:50 +00:00
this.withCredentialsRequest(
'GET',
'/projects/%username/' +
encodeURIComponent(projectName) +
(delta ? '?delta=' + delta : ''),
2017-10-04 09:46:50 +00:00
onSuccess,
onError,
'Could not fetch project ' + projectName,
true
);
};
2018-02-08 08:36:16 +00:00
Cloud.prototype.getPublicProject = function (
projectName,
username,
onSuccess,
onError
) {
this.request(
'GET',
'/projects/' +
encodeURIComponent(username) +
'/' +
encodeURIComponent(projectName),
onSuccess,
onError,
'Could not fetch project ' + projectName,
2017-10-04 09:46:50 +00:00
true
);
};
2018-02-08 08:36:16 +00:00
Cloud.prototype.getProjectMetadata = function (
projectName,
username,
onSuccess,
onError
) {
2017-10-09 10:37:34 +00:00
this.request(
'GET',
'/projects/' +
encodeURIComponent(username) +
'/' +
encodeURIComponent(projectName) +
'/metadata',
2017-10-09 10:37:34 +00:00
onSuccess,
onError,
'Could not fetch metadata for ' + projectName
);
};
Cloud.prototype.getProjectVersionMetadata = function (
projectName,
onSuccess,
onError
) {
this.withCredentialsRequest(
'GET',
'/projects/%username/' +
encodeURIComponent(projectName) +
'/versions',
onSuccess,
onError,
'Could not fetch versions for project ' + projectName
);
};
2018-02-08 08:36:16 +00:00
Cloud.prototype.deleteProject = function (
projectName,
username,
onSuccess,
onError
) {
2017-11-03 10:30:07 +00:00
this[username ? 'request' : 'withCredentialsRequest'](
2017-10-04 09:46:50 +00:00
'DELETE',
'/projects/' +
(username ? encodeURIComponent(username) : '%username') +
'/' +
encodeURIComponent(projectName),
2017-10-04 09:46:50 +00:00
onSuccess,
onError,
'Could not delete project'
);
2017-09-28 16:30:13 +00:00
};
2018-02-08 08:36:16 +00:00
Cloud.prototype.shareProject = function (
projectName,
username,
onSuccess,
onError
) {
2017-11-03 10:30:07 +00:00
this[username ? 'request' : 'withCredentialsRequest'](
'POST',
2018-02-08 08:36:16 +00:00
'/projects/' +
(username ? encodeURIComponent(username) : '%username') +
'/' +
encodeURIComponent(projectName) +
2018-02-08 08:36:16 +00:00
'/metadata?ispublic=true',
onSuccess,
onError,
'Could not share project'
);
};
2018-02-08 08:36:16 +00:00
Cloud.prototype.unshareProject = function (
projectName,
username,
onSuccess,
onError
) {
2017-11-03 10:30:07 +00:00
this[username ? 'request' : 'withCredentialsRequest'](
'POST',
2018-02-08 08:36:16 +00:00
'/projects/' +
(username ? encodeURIComponent(username) : '%username') +
'/' +
encodeURIComponent(projectName) +
2018-02-08 08:36:16 +00:00
'/metadata?ispublic=false&ispublished=false',
onSuccess,
onError,
'Could not unshare project'
);
};
2018-02-08 08:36:16 +00:00
Cloud.prototype.publishProject = function (
projectName,
username,
onSuccess,
onError
) {
2017-11-03 10:30:07 +00:00
this[username ? 'request' : 'withCredentialsRequest'](
'POST',
2018-02-08 08:36:16 +00:00
'/projects/' +
(username ? encodeURIComponent(username) : '%username') +
'/' +
encodeURIComponent(projectName) +
2018-02-08 08:36:16 +00:00
'/metadata?ispublished=true',
onSuccess,
onError,
'Could not publish project'
);
};
2018-02-08 08:36:16 +00:00
Cloud.prototype.unpublishProject = function (
projectName,
username,
onSuccess,
onError
) {
2017-11-03 10:30:07 +00:00
this[username ? 'request' : 'withCredentialsRequest'](
'POST',
2018-02-08 08:36:16 +00:00
'/projects/' +
(username ? encodeURIComponent(username) : '%username') +
'/' +
encodeURIComponent(projectName) +
2018-02-08 08:36:16 +00:00
'/metadata?ispublished=false',
onSuccess,
onError,
'Could not unpublish project'
);
};
2018-03-13 16:01:03 +00:00
Cloud.prototype.remixProject = function (
projectName,
username,
onSuccess,
onError
) {
this.withCredentialsRequest(
'POST',
'/projects/' +
encodeURIComponent(username) +
'/' +
encodeURIComponent(projectName) +
2018-03-13 16:01:03 +00:00
'/remix',
onSuccess,
onError,
'Could not remix project'
);
};
2018-02-08 08:36:16 +00:00
Cloud.prototype.updateNotes = function (
projectName,
notes,
onSuccess,
onError
) {
2017-10-25 14:59:58 +00:00
this.withCredentialsRequest(
'POST',
'/projects/%username/' +
encodeURIComponent(projectName) +
'/metadata',
2017-10-25 14:59:58 +00:00
onSuccess,
onError,
'Could not update project notes',
false, // wants raw response
JSON.stringify({ notes: notes })
);
};