Google spreadsheet get parameters from URL - javascript

I have an app, that links one service with Google Sheets by OAuth 1.0.
I click "login" in addon menu, send signature and callback domain (current sheet). Then in service I click on button, get request token and it returns me to the specified domain with parameters.
function onOpen(e)
{
Logger.log( SpreadsheetApp.getActiveSpreadsheet().getUrl() );
Logger.log( e.source.getUrl() );
}
But .getUrl() doesn't contain them.
According to >this< I can't use doGet(e) in Sheets and, because of OAuth, I can't use Web App, because I still need to pass these parameters to Sheets.
I tried to get it on client-side by window.top.location.href, but had cross-domain errors.
Question: Can I get it? Or is it impossible?

So, a partial answer to my question:
We need 2 different scripts. The first as Sheets web addon, the second as Web App. Next, we need to implement all the steps presented in the screenshot:
1) After steps 0, 1, 2 Service Provider should redirect us to Helper script (Web App) with the following code:
function doGet(e)
{
var token = e.parameter.oauth_token,
code = e.parameter.oauth_verifier,
response;
if(token && code && Root.setVerificationCode(token, code))
response = "Success";
else
response = 'Error';
return HtmlService.createHtmlOutput(response);
}
Here we retrieving GET parameters and send them to Sheets web addon (Root).
Click Resources -> Libraries and add your addon as library. Root is just an identifier.
2) Sheets addon code:
function setVerificationCode(token, code)
{
var oauth = new OAuth();
if(oauth.getAccessToken(code))
{
// change menu here
return true;
}
else
return false;
}
These are full steps you need to implement OAuth 1.0.
P.S. Now I have problems with changing menu, but it'll be my next question.

Related

Monitor google sheet change event externally [duplicate]

What I wish to achieve:
Whenever a cell is changed in any google sheet on my shared drive (by
any user on the domain) I want to call an API endpoint and include
information about which cell was edited.
My approach:
I believe Google App Scripts Add-on is what I need. Installed for all users on the domain.
I see there are "bound" scripts and standalone scripts. For standalone scripts I am not able to create any other triggers than timer and calender based triggers. Bound scripts seem to be permanently bound to a single sheet and won't impact other sheets in any way.
What am I missing?
I find a few end-to-end tutorials on blogs for making bound scripts, but nothing for generic cross-domain stuff.
You can achieve all this through a standalone script. Create a standalone script and follow these steps:
Step 1: Get spreadsheet ids
First you would have to get the id of the different Spreadsheets in your shared drive. You can do it in Google Apps Script itself if you use the Advanced Drive Service (see Reference below). To activate this service, go to Resources > Advanced Google services... in your script editor and enable Drive API.
Then, write a function that will return an array of the spreadsheet ids in the shared drive. You will have to call Drive.Files.list for that. It could be something along the following lines (please write your shared driveId in the corresponding line):
function getFileIds() {
var params = {
corpora: "drive",
driveId: "your-shared-drive-id", // Please change this accordingly
includeItemsFromAllDrives: true,
q: "mimeType = 'application/vnd.google-apps.spreadsheet'",
supportsAllDrives: true
}
var files = Drive.Files.list(params)["items"];
var ids = files.map(function(file) {
return file["id"];
})
return ids;
}
Step 2: Create triggers for each spreadsheet
Install an onEdit trigger programmatically for each of the spreadsheets (an edit trigger fires a function every time the corresponding spreadsheet is edited, so I assume this is the trigger you want). For this, the ids retrieved in step 1 will be used. It could be something similar to this:
function createTriggers(ids) {
ids.forEach(function(id) {
var ss = SpreadsheetApp.openById(id);
createTrigger(ss);
})
}
function createTrigger(ss) {
ScriptApp.newTrigger('sendDataOnEdit')
.forSpreadsheet(ss)
.onEdit()
.create();
}
The function createTriggers gets an array of ids as a parameter and, for each id, creates an onEdit trigger: everytime any of these spreadsheets is edited, the function sendDataOnEdit will run, and that's where you will want to call your API endpoint with information about the edited cell.
Step 3: Call API endpoint
The function sendDataOnEdit has to get data from the edited cell and send it somewhere.
function sendDataOnEdit(e) {
// Please fill this up accordingly
var range = e.range;
var value = range.getValue();
UrlFetchApp.fetch(url, params) // Please fill this up accordingly
}
First, it can get information about the cell that was edited via the event object, passed to the function as the parameter e (you can get its column, its row, its value, the sheet and the spreadsheet where it is located, etc.). For example, to retrieve the value of the cell you can do e.range.getValue(). Check the link I provide in reference to get more details on this.
Second, when you have correctly retrieved the data you want to send, you can use UrlFetchApp.fetch(url, params) to make a request to your URL. In the link I provide below, you can see the parameters you can specify here (e.g., HTTP method, payload, etc.).
Please bear in mind that you might need to grant some authorization to access the API endpoint, if this is not public. Check the OAuth reference I attach below.
(You have to edit this function accordingly to retrieve and send exactly what you want. What I wrote is an example).
Summing this up:
In order to create the triggers you should run createTriggers once (if you run it more times, it will start creating duplicates). Run for example, this function, that first gets the file ids via Drive API and then creates the corresponding triggers:
function main() {
var ids = getFileIds();
createTriggers(ids);
}
Also, it would be useful to have a function that will delete all the triggers. Run this in case you want to start from fresh and make sure you don't have duplicates:
function deleteTriggers() {
var triggers = ScriptApp.getProjectTriggers();
triggers.forEach(function(trigger) {
ScriptApp.deleteTrigger(trigger);
})
}
Reference:
Advanced Drive Service
Drive.Files.list
onEdit trigger
Install trigger programmatically
onEdit event object
UrlFetchApp.fetch(url, params)
Connecting to external APIs
OAuth2 for Apps Script
ScriptApp.deleteTrigger(trigger)
I hope this is of any help.

Unity and Google OAuth2 Authorization Code Flow

I am trying to build a Unity application to be deployed with WebGL. I am trying to incorporate Google Sign-In into the application, and so far, this was what I've managed to make work in the Unity WebGL build in Chrome:
User presses on the "Login with Google" button on Unity application, in Tab A.
User is directed to Google Sign In page on another Tab B.
User signs in with Google account, and is redirected to my redirect_uri, which is simply https://localhost, with the auth code parameter.
My question is, is it possible for me to do the following, possible with .jslib files:
Instead of going to redirect_uri on Tab B, instead go back to Tab A without reloading, passing along the auth code.
Building on the line above, have javascript handlers, that:
When auth code is received, initiate request to exchange auth code for the id_token as instructed here.
When id_token is received, call a C# Script function to do further actions with the id_token.
Alternatively, I can set redirect_uri to be an endpoint on my backend server, and perform the auth token -> id_token flow using the Google client SDKs. However, for this approach, I would like to know if i am able to
After the auth token -> id_token flow is completed on the backend server, close the current window, Tab B, and go back to Tab A.
After we’re back on Tab A, redirect Unity to a specific scene (not the login scene anymore, but a home page that users are directed to after they are authenticated).
Would very much appreciate any help i can get :')
EDIT: For better clarity, what I want to achieve is something that FacebookSDK for Unity has done in their FB.LogInWithReadPermissions(). The whole auth code -> access_token flow is seamless, and i get redirected back to the Unity application in Tab A at the end with the access_token.
I managed to find a Javascript solution to achieve my first method. The differences are that because
My application will never be in production
Consistency with my Facebook OAuth implementation,
I used the implicit flow instead of the authorization code flow, despite it being not the recommended way due to security concerns. However, I think you can easily use the authorization code flow, retrieving the authorization code and passing it on to your backend to exchange for an id token. (as far as I know, you cannot use Javascript/XHR requests to do this exchange)
So, the flow is that from my C# script, I call a Javascript function from a .jslib file. Basically, the function detects when the OAuth window has redirected back to my redirect_uri, then gets the access_token parameter from the redirected URI, and calls a C# Script function. From there, you should be able to do whatever you need to do (change scene, send to your backend, etc.). Note that there is a try/catch because there will be errors if you attempt to get information from the Google Sign In pages.
The file is as follows:
mergeInto(LibraryManager.library, {
OpenOAuthInExternalTab: function (url, callback) {
var urlString = Pointer_stringify(url);
var callbackString = Pointer_stringify(callback);
var child = window.open(urlString, "_blank");
var interval = setInterval(function() {
try {
// When redirected back to redirect_uri
if (child.location.hostname === location.hostname) {
clearInterval(interval) // Stop Interval
// // Auth Code Flow -- Not used due to relative complexity
// const urlParams = new URLSearchParams(child.location.search);
// const authCode = urlParams.get('code');
// console.log("Auth Code: " + authCode.toString());
// console.log("Callback: " + callbackString);
// window.unityInstance.SendMessage('Auth', callbackString, authCode);
// Implicit Flow
var fragmentString = child.location.hash.substr(1);
var fragment = {};
var fragmentItemStrings = fragmentString.split('&');
for (var i in fragmentItemStrings) {
var fragmentItem = fragmentItemStrings[i].split('=');
if (fragmentItem.length !== 2) {
continue;
}
fragment[fragmentItem[0]] = fragmentItem[1];
}
var accessToken = fragment['access_token'] || '';
console.log("access_token: " + accessToken);
child.close();
// Invoke callback function
window.unityInstance.SendMessage('Auth', callbackString, accessToken);l
}
}
catch(e) {
// Child window in another domain
console.log("Still logging in ...");
}
}, 50);
}
});
Then, in my C# script, I call this function using the following:
public class GoogleHelper : MonoBehaviour
{
[DllImport("__Internal")]
private static extern void OpenOAuthInExternalTab(string url, string callbackFunctionName);
// ...
public void Login(string callbackFunctionName) {
var redirectUri = "https://localhost";
var url = "https://accounts.google.com/o/oauth2/v2/auth"
+ $"?client_id={clientId}"
+ "&response_type=token"
+ "&scope=openid%20email%20profile"
+ $"&redirect_uri={redirectUri}";
OpenOAuthInExternalTab(url, callbackFunctionName);
}
// ...
}
Of course, this is super hacky, and I'm not very familiar with Javascript and so don't really know the implication of the code above, but it works for my use case.

Google Picker API with a valid access_token asks for sign in on first use

Firstly, I have reviewed multiple SO questions relating to similar issues and the Google Picker API docs but cannot find a solution. Links at the bottom of this post.
Goal
After using the Google auth2 JavaScript library to complete a ux_mode='popup' Sign In process for a user to obtain an access_code, I wish to open a Google Picker window using the picker JavaScript library and the obtained access_code.
Expected
After checking the access_code is still valid, when the google.picker.PickerBuilder() object is set to visible via picker.setVisible(true) the user is ALWAYS able to select files as per the configuration set for the picker object.
Actual
On a fresh browser, encountering this flow results in the Google Picker window asking the user to sign in again. If the user chooses to do so an additional popup will be triggered that automatically executes a login flow again and the user is then informed that "The API developer key is invalid."
What is truly unexpected about this is that if the user refreshes the page and repeats the exact same actions the Google Picker works exactly as expected. The only way to replicate the anomalous behaviour is to clear all the browser cache, cookies and any other site related settings.
On the JavaScript console there are the common errors of:
Failed to execute ‘postMessage’ on ‘DOMWindow’: The target origin provided (‘https://docs.google.com’) does not match the recipient window’s origin (‘http://localhost’).
Invalid X-Frame-Options header was found when loading “https://docs.google.com/picker?protocol=gadgets&origin=http%3A%2F%2Flocalhost&navHidden=true&multiselectEnabled=true&oauth_token=.............: “ALLOW-FROM http://localhost” is not a valid directive.
But otherwise, no other indication of error in the console, and the exact same errors are reported when the Picker works exactly as expected.
List of things I have confirmed
I have added the Google Picker API to the associated project in the Google developer console
I am working with a validated OAuth application configured in the OAuth Consent Screen
I have tried working with both localhost and an https:// top level domain with valid cert and registered with the Google console as verified
I have generated an API Key that is explicitly associated with the Picker API and the relevant URLs
The API key is set as the .setDeveloperKey(APIKey)
The API key does show usage in the GCP console
The clientId is correct and the appId is correct for the GCP project
I have tried with scopes of ['https://www.googleapis.com/auth/drive.file'] and with scopes of ['openid', 'email', 'profile', 'https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/documents']
Attempts to Resolve
I can replicate this behaviour with the bare minimum example provided in the docs Google Picker Example used as a direct cut and paste, only replacing the required credential strings.
Right before invoking the picker = new google.picker.PickerBuilder() I have validated the access_token by executing a GET fetch to https://www.googleapis.com/oauth2/v1/tokeninfo and the signin behavior still results when this returns a successful validation of the token.
I check the token using this simple function:
function checkToken(access_token) {
fetch("https://www.googleapis.com/oauth2/v1/tokeninfo", {
method: "GET",
headers: {
'Authorization': 'Bearer ' + access_token
}
}).then(response => {
if (response.status === 200) {
return response.json();
} else {
console.log('User Token Expired');
resetLoginCache();
}
}).then(data => {
console.log('User Token Still Valid');
}).catch((error) => {
console.error('User Token Check Error:', error);
})
}
The JavaScript API's are initialized with:
<meta name="google-signin-client_id" content="<clientId>">
<script type="text/javascript" src="https://apis.google.com/js/api.js"></script>
function initApi() {
gapi.load('signin2:auth2:picker', () => {
window.gapi.auth2.init(
{
'client_id': clientId,
'scope': scope.join(" ")
});
});
};
In my application code I've (poorly) implemented a simple generalization of a picker builder:
// Use the Google API Loader script to load the google.Picker script.
function loadPicker( mimeTypes = null, callback = pickerCallback ) {
gapi.load('picker', {
'callback': () => {
console.log("Gonna build you a new picker...")
createPicker( mimeTypes, callback );
}
});
}
// Create and render a Picker object for searching images.
function createPicker( mimeTypes, callback ) {
let gAuthTop = gapi.auth2.getAuthInstance();
let gUser = gAuthTop.currentUser.get();
let gAuthResp = gUser.getAuthResponse(true);
console.log(gAuthResp.access_token);
checkToken(gAuthResp.access_token)
if (pickerApiLoaded && oauthToken) {
// based on MIME type:
// FOLDER => google.picker.DocsView(google.picker.ViewId.FOLDERS)
// Cannot set mimeTypes to filter view
// DOCS => google.picker.View(google.picker.ViewId.DOCS)
// Can set MIME types to match
let selectView = new google.picker.View(google.picker.ViewId.DOCS);
if (mimeTypes) {
if (mimeTypes.includes(gdriveFolderMIME)) {
selectView = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
selectView.setIncludeFolders(true);
selectView.setSelectFolderEnabled(true);
selectView.setParent('root');
} else {
selectView.setMimeTypes(mimeTypes);
}
}
let picker = new google.picker.PickerBuilder()
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.enableFeature(google.picker.Feature.MINE_ONLY)
.setAppId(appId)
.setMaxItems(1)
.setOAuthToken(gAuthResp.access_token)
.addView(selectView)
.setSelectableMimeTypes(mimeTypes)
.setDeveloperKey(developerKey)
.setOrigin(window.location.protocol + '//' + window.location.host)
.setCallback(callback)
.setTitle('Application Title')
.build();
console.log('Origin was set to: ', window.location.protocol + '//' + window.location.host)
picker.setVisible(true);
}
}
I've even tried to dig into the minified code loaded by the Picker but I'm not that good at JavaScript and the Firefox debugger wasn't able to help me understand what might be triggering this. However, once the "error" has been passed once, it will not appear on the same browser again for any user account and within Firefox using Private Mode will also no longer show the sign in error until all history, cookies and cache are cleared.
As proof I have done some reasonable research, similar SO questions that I have reviewed and tried working with are:
Google picker asking to sign in even after providing access token
The API developer key is invalid when viewing file in google picker
Is it possible to open google picker with access token which is fetched from server side Oauth2?
How do I use Google Picker to access files using the “drive.file” scope?
Google Picker with AccessToken not working
Google Picker API sign in
Picker API - manually set access_token
Google Picker API - how to load a picker on demand
As well as the following documentation:
G Suite Developer Picker Documentation
Google Sign-In JavaScript client reference
Cannot find any related issue on the tracker

Content Service for Google Apps Script returning HTML instead of JSON

Trying out the Content Service API and the example is returning HTML when it should be returning JSON, what am I doing wrong?
https://developers.google.com/apps-script/guides/content
function doGet(request) {
var events = CalendarApp.getEvents(
new Date(Number(request.parameters.start) * 1000),
new Date(Number(request.parameters.end) * 1000));
var result = {
available: events.length == 0
};
return ContentService.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}
GAS from another file trying to make the request:
function myFunction() {
var url = "published URL";
url+="?start=1325437200&end=1325439000";
var options = {
method:"get"
}
var response = UrlFetchApp.fetch(url,options).getContentText();
response = JSON.parse(response); //error, unexpected token <
}
Your usage of ContentService is correct, the code works exactly as is. Here is a link to my copy of your code published as a web app:
https://script.google.com/macros/s/AKfycbzx2L643LHw0oQAq1jBmKQh2ju_znGfmdj78dUypj36iF-s91w/exec
The problem you are running into is related to Authorization or Authentication, if the published script is not authorized, an HTML error message is returned.
To check if that is your issue, simply access the published URL directly in your browser. If you see JSON displayed, then Authorization is not the problem. If you see the "Authorization is required to perform that action" error message, open your published script and choose "doGet" from the Run menu, then follow the authorization prompts.
More likely, the problem is related to how your script is published. In order to access your published script from another script, it must be published with the "Who has access to the app" setting as "Anyone, Even anonymous". If you use any other value, Google returns an HTML login page instead of your JSON response, and you get the error you are seeing.
This happens because requests sent from Google Apps Script via URLFetchApp are not authenticated, they don't carry the credentials of the user running the code with them, and come in as anonymous requests.
If you don't allow "Anyone, even anonymous" in your publishing settings, Google redirects non-authenticated requests to the Google login page.

Retrieving an object/resource from a URL

I recently started using Google Apps Script to automate some Google Analytics reporting tasks. Many objects returned by Google Analytics Services have 'get' functions that return a URL in the form of a string. Actually, many Google Apps Script objects have functions that return these resource URLs, not just Google's Analytics Services. The indication is that I can somehow use these URLs to reference Google Apps resources and get objects in return, but I don't know how.
I tried simply loading one of these URLs in a browser expecting JSON or something else I could use, but received a 404 error instead. The same happened when I tried requesting the URL using a Google Apps Script UrlFetchApp.
Here's a simple example:
var accountId = '0123456789'; // Pretend this is a valid account number
// Get the first WebProperty
var webProp = Analytics.Management.Webproperties.list(accountId).getItems()[0];
// ...getHref() is along the lines of "https://www.googleapis.com/analytics/v3/management/accounts/0123456789"
var parentHref = webProp.getParetLink().getHref();
The question is, what do I do with parentHref to get an Analytics Account object back? I feel like I'm missing something that should be fairly basic...
Resources:
Google Apps Script Reference for Analytics Services
Read Using OAuth 2.0 to Access Google APIs, will help you get what you need.
I add a very basic code that can serve as a guide.
/* CODE FOR DEMONSTRATION PURPOSES */
function authorizeGAS() {
var result = false;
var oauthConfig = UrlFetchApp.addOAuthService("google");
oauthConfig.setAccessTokenUrl("https://www.google.com/accounts/OAuthGetAccessToken");
oauthConfig.setRequestTokenUrl("https://www.google.com/accounts/OAuthGetRequestToken?" +
"scope=https://www.googleapis.com/auth/analytics.readonly");
oauthConfig.setAuthorizationUrl("https://www.google.com/accounts/OAuthAuthorizeToken");
oauthConfig.setConsumerKey(ScriptProperties.getProperty("YOUR_ANALYTICS_CONSUMER_KEY"));
oauthConfig.setConsumerSecret(ScriptProperties.getProperty("YOUR_ANALYTICS_CONSUMER_SECRET"));
var requestData = {
"method": "GET",
"oAuthServiceName": "google",
"oAuthUseToken": "always"
};
var URL = "https://www.googleapis.com/analytics/v3/management/accounts/~all/webproperties/~all/profiles";
try {
result = JSON.parse(UrlFetchApp.fetch(URL, requestData).getContentText());
} catch (e) {
if (e.message) Logger.log(e.message);
}
Logger.log(result);
return result;
}
It's not about activating the API. It's obviously active since he is already getting analytics data.
You can't just urlfetch those URLs, because you are missing the OAuth-related code that goes with calling a URL that requires authentication and permission.

Categories