mirror of
https://github.com/scratchfoundation/scratch-blocks.git
synced 2025-08-28 22:10:31 -04:00
500 lines
15 KiB
JavaScript
500 lines
15 KiB
JavaScript
/**
|
|
* @license
|
|
* Visual Blocks Editor
|
|
*
|
|
* Copyright 2013 Google Inc.
|
|
* https://developers.google.com/blockly/
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview Common utility functionality for Google Drive Realtime API,
|
|
* including authorization and file loading. This functionality should serve
|
|
* mostly as a well-documented example, though is usable in its own right.
|
|
*
|
|
* You can find this code as part of the Google Drive Realtime API Quickstart at
|
|
* https://developers.google.com/drive/realtime/realtime-quickstart and also as
|
|
* part of the Google Drive Realtime Playground code at
|
|
* https://github.com/googledrive/realtime-playground/blob/master/js/realtime-client-utils.js
|
|
*/
|
|
'use strict';
|
|
|
|
/**
|
|
* Realtime client utilities namespace.
|
|
*/
|
|
goog.provide('rtclient');
|
|
|
|
|
|
/**
|
|
* OAuth 2.0 scope for installing Drive Apps.
|
|
* @const
|
|
*/
|
|
rtclient.INSTALL_SCOPE = 'https://www.googleapis.com/auth/drive.install';
|
|
|
|
/**
|
|
* OAuth 2.0 scope for opening and creating files.
|
|
* @const
|
|
*/
|
|
rtclient.FILE_SCOPE = 'https://www.googleapis.com/auth/drive.file';
|
|
|
|
/**
|
|
* OAuth 2.0 scope for accessing the appdata folder, a hidden folder private
|
|
* to this app.
|
|
* @const
|
|
*/
|
|
rtclient.APPDATA_SCOPE = 'https://www.googleapis.com/auth/drive.appdata';
|
|
|
|
/**
|
|
* OAuth 2.0 scope for accessing the user's ID.
|
|
* @const
|
|
*/
|
|
rtclient.OPENID_SCOPE = 'openid';
|
|
|
|
/**
|
|
* MIME type for newly created Realtime files.
|
|
* @const
|
|
*/
|
|
rtclient.REALTIME_MIMETYPE = 'application/vnd.google-apps.drive-sdk';
|
|
|
|
/**
|
|
* Key used to store the folder id of the Drive folder in which we will store
|
|
* Realtime files.
|
|
* @type {string}
|
|
*/
|
|
rtclient.FOLDER_KEY = 'folderId';
|
|
|
|
/**
|
|
* Parses the hash parameters to this page and returns them as an object.
|
|
* @return {!Object} Parameter object.
|
|
*/
|
|
rtclient.getParams = function() {
|
|
// Be careful with regards to node.js which has no window or location.
|
|
var location = goog.global['location'] || {};
|
|
var params = {};
|
|
function parseParams(fragment) {
|
|
// Split up the query string and store in an object.
|
|
var paramStrs = fragment.slice(1).split('&');
|
|
for (var i = 0; i < paramStrs.length; i++) {
|
|
var paramStr = paramStrs[i].split('=');
|
|
params[decodeURIComponent(paramStr[0])] = decodeURIComponent(paramStr[1]);
|
|
}
|
|
}
|
|
var hashFragment = location.hash;
|
|
if (hashFragment) {
|
|
parseParams(hashFragment);
|
|
}
|
|
// Opening from Drive will encode the state in a query search parameter.
|
|
var searchFragment = location.search;
|
|
if (searchFragment) {
|
|
parseParams(searchFragment);
|
|
}
|
|
return params;
|
|
};
|
|
|
|
/**
|
|
* Instance of the query parameters.
|
|
*/
|
|
rtclient.params = rtclient.getParams();
|
|
|
|
/**
|
|
* Fetches an option from options or a default value, logging an error if
|
|
* neither is available.
|
|
* @param {!Object} options Containing options.
|
|
* @param {string} key Option key.
|
|
* @param {*=} opt_defaultValue Default option value (optional).
|
|
* @return {*} Option value.
|
|
*/
|
|
rtclient.getOption = function(options, key, opt_defaultValue) {
|
|
if (options.hasOwnProperty(key)) {
|
|
return options[key];
|
|
}
|
|
if (opt_defaultValue === undefined) {
|
|
console.error(key + ' should be present in the options.');
|
|
}
|
|
return opt_defaultValue;
|
|
};
|
|
|
|
/**
|
|
* Creates a new Authorizer from the options.
|
|
* @constructor
|
|
* @param {!Object} options For authorizer. Two keys are required as mandatory,
|
|
* these are:
|
|
*
|
|
* 1. "clientId", the Client ID from the console
|
|
* 2. "authButtonElementId", the is of the dom element to use for
|
|
* authorizing.
|
|
*/
|
|
rtclient.Authorizer = function(options) {
|
|
this.clientId = rtclient.getOption(options, 'clientId');
|
|
// Get the user ID if it's available in the state query parameter.
|
|
this.userId = rtclient.params['userId'];
|
|
this.authButton = document.getElementById(rtclient.getOption(options,
|
|
'authButtonElementId'));
|
|
this.authDiv = document.getElementById(rtclient.getOption(options,
|
|
'authDivElementId'));
|
|
};
|
|
|
|
/**
|
|
* Start the authorization process.
|
|
* @param {Function} onAuthComplete To call once authorization has completed.
|
|
*/
|
|
rtclient.Authorizer.prototype.start = function(onAuthComplete) {
|
|
var _this = this;
|
|
gapi.load('auth:client,drive-realtime,drive-share', function() {
|
|
_this.authorize(onAuthComplete);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Reauthorize the client with no callback (used for authorization failure).
|
|
* @param {Function} onAuthComplete To call once authorization has completed.
|
|
*/
|
|
rtclient.Authorizer.prototype.authorize = function(onAuthComplete) {
|
|
var clientId = this.clientId;
|
|
var userId = this.userId;
|
|
var _this = this;
|
|
var handleAuthResult = function(authResult) {
|
|
if (authResult && !authResult.error) {
|
|
_this.authButton.disabled = true;
|
|
_this.fetchUserId(onAuthComplete);
|
|
_this.authDiv.style.display = 'none';
|
|
} else {
|
|
_this.authButton.disabled = false;
|
|
_this.authButton.onclick = authorizeWithPopup;
|
|
_this.authDiv.style.display = 'block';
|
|
}
|
|
};
|
|
var authorizeWithPopup = function() {
|
|
gapi.auth.authorize({
|
|
'client_id': clientId,
|
|
'scope': [
|
|
rtclient.INSTALL_SCOPE,
|
|
rtclient.FILE_SCOPE,
|
|
rtclient.OPENID_SCOPE,
|
|
rtclient.APPDATA_SCOPE
|
|
],
|
|
'user_id': userId,
|
|
'immediate': false
|
|
}, handleAuthResult);
|
|
};
|
|
// Try with no popups first.
|
|
gapi.auth.authorize({
|
|
'client_id': clientId,
|
|
'scope': [
|
|
rtclient.INSTALL_SCOPE,
|
|
rtclient.FILE_SCOPE,
|
|
rtclient.OPENID_SCOPE,
|
|
rtclient.APPDATA_SCOPE
|
|
],
|
|
'user_id': userId,
|
|
'immediate': true
|
|
}, handleAuthResult);
|
|
};
|
|
|
|
/**
|
|
* Fetch the user ID using the UserInfo API and save it locally.
|
|
* @param {Function} callback The callback to call after user ID has been
|
|
* fetched.
|
|
*/
|
|
rtclient.Authorizer.prototype.fetchUserId = function(callback) {
|
|
var _this = this;
|
|
gapi.client.load('oauth2', 'v2', function() {
|
|
gapi.client.oauth2.userinfo.get().execute(function(resp) {
|
|
if (resp.id) {
|
|
_this.userId = resp.id;
|
|
}
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a new Realtime file.
|
|
* @param {string} title Title of the newly created file.
|
|
* @param {string} mimeType The MIME type of the new file.
|
|
* @param {string} folderTitle Title of the folder to place the file in.
|
|
* @param {Function} callback The callback to call after creation.
|
|
*/
|
|
rtclient.createRealtimeFile = function(title, mimeType, folderTitle, callback) {
|
|
|
|
function insertFile(folderId) {
|
|
gapi.client.drive.files.insert({
|
|
'resource': {
|
|
'mimeType': mimeType,
|
|
'title': title,
|
|
'parents': [{'id': folderId}]
|
|
}
|
|
}).execute(callback);
|
|
}
|
|
|
|
function getOrCreateFolder() {
|
|
|
|
function storeInAppdataProperty(folderId) {
|
|
// Store folder id in a custom property of the appdata folder. The
|
|
// 'appdata' folder is a special Google Drive folder that is only
|
|
// accessible by a specific app (i.e. identified by the client id).
|
|
gapi.client.drive.properties.insert({
|
|
'fileId': 'appdata',
|
|
'resource': { 'key': rtclient.FOLDER_KEY, 'value': folderId }
|
|
}).execute(function(resp) {
|
|
insertFile(folderId);
|
|
});
|
|
};
|
|
|
|
function createFolder() {
|
|
gapi.client.drive.files.insert({
|
|
'resource': {
|
|
'mimeType': 'application/vnd.google-apps.folder',
|
|
'title': folderTitle
|
|
}
|
|
}).execute(function(folder) {
|
|
storeInAppdataProperty(folder.id);
|
|
});
|
|
}
|
|
|
|
// Get the folder id from the appdata properties.
|
|
gapi.client.drive.properties.get({
|
|
'fileId': 'appdata',
|
|
'propertyKey': rtclient.FOLDER_KEY
|
|
}).execute(function(resp) {
|
|
if (resp.error) {
|
|
// There's no folder id stored yet so we create a new folder if a
|
|
// folderTitle has been supplied.
|
|
if (folderTitle) {
|
|
createFolder();
|
|
} else {
|
|
// There's no folder specified, so we just store the file in the
|
|
// user's root folder.
|
|
storeInAppdataProperty('root');
|
|
}
|
|
} else {
|
|
var folderId = resp.result.value;
|
|
gapi.client.drive.files.get({
|
|
'fileId': folderId
|
|
}).execute(function(resp) {
|
|
if (resp.error || resp.labels.trashed) {
|
|
// Folder doesn't exist or was deleted, so create a new one.
|
|
createFolder();
|
|
} else {
|
|
insertFile(folderId);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
gapi.client.load('drive', 'v2', function() {
|
|
getOrCreateFolder();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Fetches the metadata for a Realtime file.
|
|
* @param {string} fileId The file to load metadata for.
|
|
* @param {Function} callback The callback to be called on completion,
|
|
* with signature:
|
|
*
|
|
* function onGetFileMetadata(file) {}
|
|
*
|
|
* where the file parameter is a Google Drive API file resource instance.
|
|
*/
|
|
rtclient.getFileMetadata = function(fileId, callback) {
|
|
gapi.client.load('drive', 'v2', function() {
|
|
gapi.client.drive.files.get({
|
|
'fileId': fileId
|
|
}).execute(callback);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Parses the state parameter passed from the Drive user interface after
|
|
* Open With operations.
|
|
* @param {string} stateParam The state query parameter as a JSON string.
|
|
* @return {Object} The state query parameter as an object or null if
|
|
* parsing failed.
|
|
*/
|
|
rtclient.parseState = function(stateParam) {
|
|
try {
|
|
var stateObj = JSON.parse(stateParam);
|
|
return stateObj;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handles authorizing, parsing query parameters, loading and creating Realtime
|
|
* documents.
|
|
* @constructor
|
|
* @param {!Object} options Options for loader. Four keys are required as
|
|
* mandatory, these are:
|
|
*
|
|
* 1. "clientId", the Client ID from the console
|
|
* 2. "initializeModel", the callback to call when the file is loaded.
|
|
* 3. "onFileLoaded", the callback to call when the model is first created.
|
|
*
|
|
* and two keys are optional:
|
|
*
|
|
* 1. "defaultTitle", the title of newly created Realtime files.
|
|
* 2. "defaultFolderTitle", the folder to place in which to place newly
|
|
* created Realtime files.
|
|
*/
|
|
rtclient.RealtimeLoader = function(options) {
|
|
// Initialize configuration variables.
|
|
this.onFileLoaded = rtclient.getOption(options, 'onFileLoaded');
|
|
this.newFileMimeType = rtclient.getOption(options, 'newFileMimeType',
|
|
rtclient.REALTIME_MIMETYPE);
|
|
this.initializeModel = rtclient.getOption(options, 'initializeModel');
|
|
this.registerTypes = rtclient.getOption(options, 'registerTypes',
|
|
function() {});
|
|
this.afterAuth = rtclient.getOption(options, 'afterAuth', function() {});
|
|
// This tells us if need to we automatically create a file after auth.
|
|
this.autoCreate = rtclient.getOption(options, 'autoCreate', false);
|
|
this.defaultTitle = rtclient.getOption(options, 'defaultTitle',
|
|
'New Realtime File');
|
|
this.defaultFolderTitle = rtclient.getOption(options, 'defaultFolderTitle',
|
|
'');
|
|
this.afterCreate = rtclient.getOption(options, 'afterCreate', function() {});
|
|
this.authorizer = new rtclient.Authorizer(options);
|
|
};
|
|
|
|
/**
|
|
* Redirects the browser back to the current page with an appropriate file ID.
|
|
* @param {Array.<string>} fileIds The IDs of the files to open.
|
|
* @param {string} userId The ID of the user.
|
|
*/
|
|
rtclient.RealtimeLoader.prototype.redirectTo = function(fileIds, userId) {
|
|
var params = [];
|
|
if (fileIds) {
|
|
params.push('fileIds=' + fileIds.join(','));
|
|
}
|
|
if (userId) {
|
|
params.push('userId=' + userId);
|
|
}
|
|
// Naive URL construction.
|
|
var newUrl = params.length == 0 ?
|
|
window.location.pathname :
|
|
(window.location.pathname + '#' + params.join('&'));
|
|
// Using HTML URL re-write if available.
|
|
if (window.history && window.history.replaceState) {
|
|
window.history.replaceState('Google Drive Realtime API Playground',
|
|
'Google Drive Realtime API Playground', newUrl);
|
|
} else {
|
|
window.location.href = newUrl;
|
|
}
|
|
// We are still here that means the page didn't reload.
|
|
rtclient.params = rtclient.getParams();
|
|
for (var index in fileIds) {
|
|
gapi.drive.realtime.load(fileIds[index], this.onFileLoaded,
|
|
this.initializeModel, this.handleErrors);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Starts the loader by authorizing.
|
|
*/
|
|
rtclient.RealtimeLoader.prototype.start = function() {
|
|
// Bind to local context to make them suitable for callbacks.
|
|
var _this = this;
|
|
this.authorizer.start(function() {
|
|
if (_this.registerTypes) {
|
|
_this.registerTypes();
|
|
}
|
|
if (_this.afterAuth) {
|
|
_this.afterAuth();
|
|
}
|
|
_this.load();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Handles errors thrown by the Realtime API.
|
|
* @param {!Error} e Error.
|
|
*/
|
|
rtclient.RealtimeLoader.prototype.handleErrors = function(e) {
|
|
if (e.type == gapi.drive.realtime.ErrorType.TOKEN_REFRESH_REQUIRED) {
|
|
this.authorizer.authorize();
|
|
} else if (e.type == gapi.drive.realtime.ErrorType.CLIENT_ERROR) {
|
|
alert('An Error happened: ' + e.message);
|
|
window.location.href = '/';
|
|
} else if (e.type == gapi.drive.realtime.ErrorType.NOT_FOUND) {
|
|
alert('The file was not found. It does not exist or you do not have ' +
|
|
'read access to the file.');
|
|
window.location.href = '/';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Loads or creates a Realtime file depending on the fileId and state query
|
|
* parameters.
|
|
*/
|
|
rtclient.RealtimeLoader.prototype.load = function() {
|
|
var fileIds = rtclient.params['fileIds'];
|
|
if (fileIds) {
|
|
fileIds = fileIds.split(',');
|
|
}
|
|
var userId = this.authorizer.userId;
|
|
var state = rtclient.params['state'];
|
|
// Creating the error callback.
|
|
var authorizer = this.authorizer;
|
|
// We have file IDs in the query parameters, so we will use them to load a
|
|
// file.
|
|
if (fileIds) {
|
|
for (var index in fileIds) {
|
|
gapi.drive.realtime.load(fileIds[index], this.onFileLoaded,
|
|
this.initializeModel, this.handleErrors);
|
|
}
|
|
return;
|
|
}
|
|
// We have a state parameter being redirected from the Drive UI.
|
|
// We will parse it and redirect to the fileId contained.
|
|
else if (state) {
|
|
var stateObj = rtclient.parseState(state);
|
|
// If opening a file from Drive.
|
|
if (stateObj.action == 'open') {
|
|
fileIds = stateObj.ids;
|
|
userId = stateObj.userId;
|
|
this.redirectTo(fileIds, userId);
|
|
return;
|
|
}
|
|
}
|
|
if (this.autoCreate) {
|
|
this.createNewFileAndRedirect();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates a new file and redirects to the URL to load it.
|
|
*/
|
|
rtclient.RealtimeLoader.prototype.createNewFileAndRedirect = function() {
|
|
// No fileId or state have been passed. We create a new Realtime file and
|
|
// redirect to it.
|
|
var _this = this;
|
|
rtclient.createRealtimeFile(this.defaultTitle, this.newFileMimeType,
|
|
this.defaultFolderTitle,
|
|
function(file) {
|
|
if (file.id) {
|
|
if (_this.afterCreate) {
|
|
_this.afterCreate(file.id);
|
|
}
|
|
_this.redirectTo([file.id], _this.authorizer.userId);
|
|
} else {
|
|
// File failed to be created, log why and do not attempt to redirect.
|
|
console.error('Error creating file.');
|
|
console.error(file);
|
|
}
|
|
});
|
|
};
|