I have developed a google apps script in a spreadsheet that uses oAuth.
I now copied the same script to a gadget that runs on a site. When I want to run the function that uses oauth I get the following error:
Unexpected exception upon serializing continuation
This happens both when I run the actual gadget on a site or when I run the function from the script editor. The exact same code works when called from the script editor in the spreadsheet.
Am I doing something wrong or is it simply not possible to use oAuth with UrlFetchApp.fetch when using a site gadget?
Thanks,
Jan
Here's some sample code of what I'm trying to do, you'll need to include real api secrets from the Google Api console to test it.
function CalendarApiBug( ) {
var oAuthConfig = UrlFetchApp.addOAuthService('agenda scheduler');
oAuthConfig.setRequestTokenUrl("https://www.google.com/accounts/"+
"OAuthGetRequestToken?scope=https://www.googleapis.com/auth/calendar");
oAuthConfig.setAuthorizationUrl("https://www.google.com/accounts/OAuthAuthorizeToken");
oAuthConfig.setAccessTokenUrl("https://www.google.com/accounts/OAuthGetAccessToken");
oAuthConfig.setConsumerKey('replacemewithsomethingreal');
oAuthConfig.setConsumerSecret('replacemewithsomethingreal');
this.baseUrl = 'https://www.googleapis.com/calendar/v3/';
this.calendarsList = null;
this.getBaseUrl = function() {
return this.baseUrl;
} //CalendarApiBug.getBaseUrl
this.getFetchArgs = function() {
return {oAuthServiceName:'agenda scheduler', oAuthUseToken:"always"};
} //CalendarApiBug.getFetchArgs
this.getCalendarList = function(refresh){
if (refresh != true && this.calendarsList != null )
return this.calendarsList;
var fetchArgs = this.getFetchArgs();
fetchArgs.method = 'get';
var url = this.baseUrl + 'users/me/calendarList';
this.calendarsList = Utilities.jsonParse(UrlFetchApp.fetch(url, fetchArgs).getContentText());
return this.calendarsList;
} //CalendarApiBug.getCalendarList
}
function test(){
var api = new CalendarApiBug();
Logger.log(api.getCalendarList(false));
}
The oAuth approval dialog only becomes visible when running the code from inside the Script Manager. In order to publish your Apps Script code to a Site, you would have needed to publish that version of the script as a service. Open the code editor for that script and make sure that you can run the functions with the script editor first. This will verify your oAuth approval has been stored.
Why use Oauth in a Google Apps Script for the Calendar service?
Related
I had a prob with my script, which was greatly answered in this question.
Basically custom functions cannot call services that require authorization. However, as far as I understood if I use simple triggers, such as onEdit it could work.
I checked the documentation suggested in the previous question, however I wasn't successful applying that to my code, which you can see below:
function FileName (id) {
var ss = DriveApp.getFileById(id);
return ss.getName();
}
How could I adapt my code to use simple triggers?
Here is a sample sheet that replicates the problem.
I believe your goal as follows.
You want to use your function of FileName as the custom function of Google Spreadsheet.
You want to automatically retrieve the filename when the file ID is put to the column "B".
You want to put the filename to the column "C".
Issue and workaround:
Unfortunately, when the custom function is used, in the current specification, the most methods except several methods (for example, one of them is UrlFetchApp.) that the authorization is required cannot be used. By this, DriveApp.getFileById(id) in your script cannot be used with the custom function. But there is a workaround. At the custom function, UrlFetchApp can be used. In this answer, I would like to propose to use the Web Apps with UrlFetchApp as the wrapper for authorizing. By this, the authorization can be done with the Web Apps. So your function can be run by the custom function.
Usage:
1. Prepare script.
Please copy and paste the following script to the script editor and save it.
const key = "samplekey"; // This is a key for using Web Apps. You can freely modify this.
// This is your function.
function FileName_(id) {
var ss = DriveApp.getFileById(id);
return ss.getName();
}
// Web Apps using as the wrapper for authorizing.
function doGet(e) {
let res = "";
if (e.parameter.key === key) {
try {
res = FileName_(e.parameter.id);
} catch (err) {
res = `Error: ${err.message}`;
}
} else {
res = "Key error.";
}
return ContentService.createTextOutput(JSON.stringify({value: res}));
}
function Filename(id) {
const webAppsUrl = "https://script.google.com/macros/s/###/exec"; // Please set the URL of Web Apps after you set the Web Apps.
const res = UrlFetchApp.fetch(`${webAppsUrl}?id=${id}&key=${key}`);
if (res.getResponseCode() != 200) throw new Error(res.getContentText());
return JSON.parse(res.getContentText()).value;
}
2. Deploy Web Apps.
On the script editor, Open a dialog box by "Publish" -> "Deploy as web app".
Select "Me" for "Execute the app as:".
By this, the script is run as the owner.
Select "Anyone, even anonymous" for "Who has access to the app:".
In this case, the access token is not required to request to Web Apps. But in this sample script, a key for requesting to Web Apps is used.
Click "Deploy" button as new "Project version".
Automatically open a dialog box of "Authorization required".
Click "Review Permissions".
Select own account.
Click "Advanced" at "This app isn't verified".
Click "Go to ### project name ###(unsafe)"
Click "Allow" button.
Click "OK".
Copy the URL of Web Apps. It's like https://script.google.com/macros/s/###/exec.
When you modified the Google Apps Script, please redeploy as new version. By this, the modified script is reflected to Web Apps. Please be careful this.
Please set the URL of https://script.google.com/macros/s/###/exec to url of above script. And please redeploy Web Apps. By this, the latest script is reflected to the Web Apps. So please be careful this.
3. Test this workaround.
When the file ID is put to the cell "A1", please put =filename(A1) to a cell as the custom function. By this, the script is run and the response value is returned.
Note:
Above sample script is a simple sample script for testing your script. So when you want to use the various methods, this post might be useful.
Please use this script with enabling V8.
As other method, I think that when the file ID is manually put to the column "B", the installable OnEdit trigger can be used. The sample script is as follows. Please set the sheet name. And please install the trigger to the function of installedOnEdit. Ref By this, when the file ID is put to the column "B" of sheetName, the file ID is put to the column "C".
function installedOnEdit(e) {
const sheetName = "Sheet1";
const range = e.range;
const sheet = range.getSheet();
if (!(sheet.getSheetName() == sheetName && range.getColumn() == 2 && range.getRow() > 1)) return;
const value = range.getValue();
let res = "";
try {
res = DriveApp.getFileById(value).getName();
} catch(e) {
res = e.message;
}
range.offset(0, 1).setValue(res);
}
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
Enhanced Custom Function for Google Spreadsheet using Web Apps as Wrapper
Related questions
Can you write a Google Sheets function that draws something?
Error when running Youtube Data Service in App Scripts (js) – Daily Limit for Unauthenticated Use Exceeded
How to enable not authorized users to protect the spreadsheet
Changing Owner of the Sheet irrespective of the duplicator
Installable Triggers
As you can draw from the documentation, simple triggers cannot access services that require authorization neither
You have to use installable triggers instead.
However the workflow is very different from custom functions.
In your specific case, you can implement e.g. that when a cell in column A is being edited (that is a new URL is being inserted) - the respective file name is being found and returned into column D.
You can retrieve the value and the row in which the new URL is being inserted with help of event objects.
Sample:
function FileName (event) {
var id = event.value;
var ss = DriveApp.getFileById(id);
var row = event.range.getRow();
var sheet = event.range.getSheet();
// for column D:
var column = 4;
var returnCell = sheet.getRange(row,column);
returnCell.setValue(ss.getName());
}
For using an installable onEdit trigger - bind it to this function through going on Edit > Current project's triggers as explained here.
In Google Sheets I use this code for opening a URL after a user edits some cell, but there is problem: I can open the URL in a modal window, but other editors cannot. When I review the Stackdriver logs, I see errors that state the script does not have permission to call showModalDialog().
function onEditTrigger(e) {
var eRange = SpreadsheetApp.getActiveSpreadsheet().getActive().getActiveRange();
var eRow = eRange.getRow();
var eCol = eRange.getColumn();
if (eCol == 1)
openUrl("https://stackoverflow.com");
}
function openUrl(url) {
var html = "<script>window.open('" + url + "'); google.script.host.close();</script>";
var userInterface = HtmlService.createHtmlOutput(html).setHeight(6).setWidth(9);
SpreadsheetApp.getUi().showModalDialog(userInterface, 'Opening ...');
}
I have installed an "edit" trigger for the "onEditTrigger" function. How can I make sure it works for all editors?
You can't have your script executing JS code snippets simply by sharing it with other users. Your collaborators will still need to open the Script Editor and manually re-authorize the bound script. Sure thing, Google will throw the usual warnings at them while they do it.
The OnEditTrigger will still fire but injecting client-side code will result in failure.
JS code can be used for malicious purposes and stealing private data, so I don't think Google will allow anyone to avoid the explicit authorization flow.
Whenever I attempt to display a UI dialog (e.g. msgBox or alert) it works fine when invoked via a menu item (e.g. from Google Sheets), but it hangs my script if I try to invoke it from the Google Apps Script editor (e.g. via Run > Run function).
My guess is it's because the Google Apps Script editor can't display any UI. To resolve this, I'd like to create a wrapper function that checks how the script was run, and not present UI depending on the source.
The "Executions" screen has the notion of Type (Editor, Standalone, Trigger):
This makes me think there is a way to get this type in code somehow.
Psuedo code of what the function might look like:
function showMessage(message) {
var scriptSource = ???;
if (scriptSource === "Standalone") {
Browser.msgBox(message);
} else {
console.log(message);
}
}
How would I get the scriptSource?
The closest thing I can find is TriggerSource, but that is missing the enum values 'Editor' and 'Trigger'. Furthermore, it's a property only available on a Trigger. I don't know how to access the current trigger. From my understanding, that's only available via the event object (e.g. via triggerUid) on functions acting as triggers. This method I'm running in the apps script editor doesn't have access to an event object.
Not the best solution, but my current workaround is to create 3 versions of each function, and append how it was invoked to the name.
For example, if there was a "Hello World" function:
function onOpen() {
var menu = [
{name: 'Hello World', functionName: 'helloWorldViaMenu_'},
];
SpreadsheetApp.getActive().addMenu('Custom', menu);
}
function helloWorldViaMenu_() {
helloWorld_(false);
}
function helloWorldViaEditor() {
helloWorld_(true);
}
function helloWorld_(invokedFromEditor) {
if (invokedFromEditor) {
Logger.log("Hello world");
} else {
Browser.msgBox("Hello world");
}
}
helloWorldViaEditor is the only that doesn't have a _ at the end so it can be selected via the "Select function" Editor UI dropdown.
You want to know whether the current project is the container-bound script type or the standalone script type.
You want to use Browser.msgBox().
I could understand about your question as above. In order to achieve it, as a workaround,I would like to propose to use Apps Script API. The flow of sample script is as follows. I think that there are several workarounds for your situation. So please think of this as one of them.
Retrieve the parent ID of the project using the method of projects.get in Apps Script API. The parent ID means that the file ID of Google Docs.
When the parent ID is returned, it is found that the project is the container-bound script type.
When the parent ID is NOT returned, it is found that the project is the standalone script type.
When the mimeType of parent ID is Google Form, Browser.msgBox() cannot be used. So the if statement is used for this.
Sample script:
This is a sample script. In this sample script, the script ID of current project is used. Of course, you can also manually give the script ID.
var id = ScriptApp.getScriptId(); // Retrieve scriptId of current project.
var url = "https://script.googleapis.com/v1/projects/" + id + "?fields=parentId";
var res = UrlFetchApp.fetch(url, {headers: {Authorization: "Bearer " + ScriptApp.getOAuthToken()}});
res = JSON.parse(res.getContentText());
if ("parentId" in res) {
Logger.log("Container-bound script type.")
var mimeType = DriveApp.getFileById(res.parentId).getMimeType();
if (mimeType === MimeType.GOOGLE_FORMS) {
Logger.log("Browser.msgBox() cannot be used at Google Form.");
} else {
Browser.msgBox("Hello world");
}
} else {
Logger.log("Standalone script type.")
Logger.log("Hello world");
}
Note:
When you use this script, please do the following flow.
Enable Apps Script API at API console.
At least, add the following scopes to the manifests.
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/script.external_request
https://www.googleapis.com/auth/script.projects.readonly
If in your script, other scopes are required to be added, please add them. And if you want to use the automatically installer of scopes with the script editor, you can achieve it using a library. You can see the detail information at here.
References:
Apps Script API
Manifests
projects.get
Taking Advantage of Manifests by GAS Library
If I misunderstand your question, I'm sorry.
Edit:
You want to confirm whether the function is called from the script editor or the custom menu.
If my understanding is correct, how about this sample script? This is a sample script. The process list can be retrieved by giving the script ID and function name. In this sample script, using "ProcessType" of processes.listScriptProcesses in Apps Script API, it confirms whether the function is called from the script editor or the custom menu.
Sample script:
This is a sample script. The process list can be retrieved by giving the script ID and function name.
When you use this script, please enable Apps Script API at API console, and add a scope of https://www.googleapis.com/auth/script.processes to the manifests.
The how to use this script is as follows.
Run addCustomMenu().
Run sampleFunction at the custom menu.
By this, Call from custom menu is shown in log.
Run sampleFunction at the script editor.
By this, Call from script editor is shown in log.
Script:
function addCustomMenu() {
SpreadsheetApp.getUi().createMenu('sampleCustomMenu').addItem('sample', 'sampleFunction').addToUi();
}
function sampleFunction() {
var scriptId = ScriptApp.getScriptId();
var functionName = "sampleFunction";
var url = "https://script.googleapis.com/v1/processes:listScriptProcesses?scriptId=" + scriptId + "&scriptProcessFilter.functionName=" + functionName;
var res = UrlFetchApp.fetch(url, {headers: {Authorization: "Bearer " + ScriptApp.getOAuthToken()}, muteHttpExceptions: true});
res = JSON.parse(res);
if (!("processType" in res.processes[0])) {
Logger.log("Call from custom menu")
} else if (res.processes[0].processType == "EDITOR") {
Logger.log("Call from script editor")
}
}
References:
Apps Script API
Manifests
processes.listScriptProcesses
ProcessType
Making Dialogs
You can run them from the menu or the script editor. They work the same.
function makeAmenu(){
SpreadsheetApp.getUi().createMenu('A Menu')
.addItem('Run my Dialogs', 'showMyDialogs')
.addToUi();
}
function showMyDialogs(){
var ui=SpreadsheetApp.getUi();
ui.alert('This is an alert');
ui.prompt('This is a prompt');
var html=HtmlService.createHtmlOutput('<p>This is a modeless dialog</p><input type="button" value="Close" onClick="google.script.host.close();" />');
ui.showModelessDialog(html, 'Dialog');
}
If you run a script from here:
The you have to go here to see it:
I need to execute a GAS service on behalf of a user that is logged to my system. So I have her/his access token. I would like somehow to transfer the token to the web app and without having to authorize again the user to use it for some activities. Can this be accomplished? Thank you.
EDIT: I think I didn't explain right what I try to accomplish. Here is the work flow I try to achieve:
We authorize a user visiting our website using OAuth2 and Google;
We get hold of her/his access token that Google returns;
There is a Google Apps Script web app that is executed as the user running the web app;
We want to call this app (3) by providing the access token (2) so Google not to ask again for authorization;
Actually, we want to call this app (3) not by redirecting the user to it but by calling it as a web service.
Thanks
Martin's answer worked for me in the end, but when I was making a prototype there was a major hurdle.
I needed to add the following scope manually, as the "automatic scope detection system" of google apps script did not ask for it: "https://www.googleapis.com/auth/drive.readonly". This resulted in UrlFetchApp.fetch always giving 401 with additional information I did not understand. Logging this additional information would show html, including the following string
Sorry, unable to open the file at this time.</p><p> Please check the address and try again.
I still don't really understand why "https://www.googleapis.com/auth/drive.readonly" would be necessary. It may have to do with the fact that we can use the /dev url, but who may use the /dev url is managed is checked using the drive permissions of the script file.
That said, the following setup then works for me (it also works with doGet etc, but I chose doPost). I chose to list the minimally needed scopes explicitly in the manifest file, but you can also make sure the calling script will ask for permissions to access drive in different ways. We have two google apps script projects, Caller and WebApp.
In the manifest file of Caller, i.e. appsscript.json
{
...
"oauthScopes":
[
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/script.external_request"]
}
In Code.gs of Caller
function controlCallSimpleService(){
var webAppUrl ='https://script.google.com/a/DOMAIN/macros/s/id123123123/exec';
// var webAppUrl =
// 'https://script.google.com/a/DOMAIN/macros/s/id1212121212/dev'
var token = ScriptApp.getOAuthToken();
var options = {
'method' : 'post'
, 'headers': {'Authorization': 'Bearer '+ token}
, muteHttpExceptions: true
};
var response = UrlFetchApp.fetch(webAppUrl, options);
Logger.log(response.getContentText());
}
In Code.gs of WebApp (the web app being called)
function doPost(event){
return ContentService.createTextOutput("Hello World");
}
The hard answer is NO you can't use the built-in services of Apps Script with a service token. But if you already have the token for a user generated by a service account, access to the users data is pretty similar to any other language. All calls would be to the REST interface of the service your token is scoped for.
Take this small script for example. It will build a list of all the user's folders and return them as JSON:
function doGet(e){
var token = e.parameter.token;
var folderArray = [];
var pageToken = "";
var query = encodeURIComponent("mimeType = 'application/vnd.google-apps.folder'");
var params = {method:"GET",
contentType:'application/json',
headers:{Authorization:"Bearer "+token},
muteHttpExceptions:true
};
var url = "https://www.googleapis.com/drive/v2/files?q="+query;
do{
var results = UrlFetchApp.fetch(url,params);
if(results.getResponseCode() != 200){
Logger.log(results);
break;
}
var folders = JSON.parse(results.getContentText());
url = "https://www.googleapis.com/drive/v2/files?q="+query;
for(var i in folders.items){
folderArray.push({"name":folders.items[i].title, "id":folders.items[i].id})
}
pageToken = folders.nextPageToken;
url += "&pageToken="+encodeURIComponent(pageToken);
}while(pageToken != undefined)
var folderObj = {};
folderObj["folders"] = folderArray;
return ContentService.createTextOutput(JSON.stringify(folderObj)).setMimeType(ContentService.MimeType.JSON);
}
You do miss out on a lot of the convenience that makes Apps Script so powerful, mainly the built in services, but all functionality is available through the Google REST APIs.
I found a way! Just include the following header in the request:
Authorization: Bearer <user's_access_token>
I am trying to display a Google Visualization GeoMap on my website. I created the code in the Code Playground and saved it as an Apps Script in my website. The page goes through the load process but nothing is displayed.
Here is the code:
function doGet() {
var app = UiApp.createApplication();
function drawVisualization() {
var query = new google.visualization.Query(
'https://docs.google.com/a/mantisnetworks.co/spreadsheet/ccc?key=0AoNHsySj5NxGdGt0dmxva3ZPb3dLYVpVZ2Z4TThNbGc&usp=drive_web#gid=0');
query.send(handleQueryResponse);
}
function handleQueryResponse(response){
if(response.isError()){
alert('Error in Query:' + response.getMessage()+''+response.getDetailedMessage());
return;
}
var data = response.getDataTable();
var geochart = new google.visualization.GeoMap(document.getElementById('visualization'));
var options = {};
options['dataMode'] = 'regions';
options['resolution'] = 'provinces';
options['region'] = 'LS';
options['width'] = '600px';
options['height'] = '300px';
geochart.draw(data, options);
}
app.close();
return app;
}
Google Apps Script is based on Javascript, but as a server-side environment it does not have access to all client-side javascript constructs. The Google visualizations, for instance, are provided as the Charts Service. Using that service, you'll find support for much of the visualization API. However, you won't find GeoMap.
The code you've provided in your question needs to be reworked considerably to work properly in Google Apps Script. Start with the example given on the Charts Service page, then adapt to your situation.
You do have another alternative within Google Apps Script, which is to use the HTML service to "host" an HTML page containing "real" javascript. Javascript that's embedded in HTML pages can be made to run on the client browser, so the example you cooked up in the playground should work. A full run-down of this option is beyond the scope of your question, but if you're interested in it you could start by scanning previous questions about the HTML Service.