ScriptDb usage from WebApp - google-apps-script

When using ScriptDb from within a WebApp that can be accessed by anyone and runs as the accessing user together with Triggers from within that WebApp it seems as if the user can not be correctly determined.
Google Apps Script as WebApp for Anyone running as the accessing user
ScriptApp.newTrigger("myfunc").timeBased().everyMinutes(1).create();
Where function myfunc is:
var q = {
user: Session.getActiveUser().getEmail()
};
result = db.query(q).sortBy('when', db.ASCENDING);
result seems to be empty when myfunc is being accessed from the trigger.
Shouldn't the active user within the trigger be the one that installed it? E.g. the user accessing the WebApp when first giving the authorization?

The Session.getEffectiveUser method returns the correct user email, as expected. Since when the script is running from a trigger, there's no one "active" using it. The "effective" user though is always set, as it is the account which the script is running "under". Here is a sample web application which emails an expected email address in the subject and body.
function doGet() {
var trigger = ScriptApp.newTrigger("testTrigger").timeBased().everyMinutes(1).create();
var app = UiApp.createApplication();
app.add(app.createLabel("test"));
return app;
}
function testTrigger(e) {
MailApp.sendEmail("xxx#sample.com", "Subject: " + Session.getEffectiveUser().getEmail(), "Body: " + Session.getEffectiveUser().getEmail());
}

Related

Call a web-app script within a script with UrlFetchApp.fetch

I have a script deployed as a webapp. It is asking too many permissions to the user, so I want to divide the script in two parts. One will be run as the user so that I can get his/her email, the other one will be run as me to edit a spreadsheet.
I have tried to use the UrlFetchApp.fetch. I do not get an error, but the second script does not write in a cell as it is supposed to do. I am not sure what is wrong.
Webapp1 - accessed by user (showing only one relevant function. There is more not relevant code which I excluded to show a simple example):
EDITED CODE:
function ServerSideFunc() {
var ss = SpreadsheetApp.openById('1kLZ3CHPkODHRc_judkUSJ3ocWVPh6nIIjJS7TLLejIg');
var sh = ss.getSheetByName('Database');
var response =UrlFetchApp.fetch("https://script....../macros/s/.....s/exec");
}
Webapp2: (the one published in the URL above and run as me)
function test() {
var i = 408 //I would like to pass this as parameter eventually from webapp1
var ss = SpreadsheetApp.openById('1kLZ3CHPkODHRc_judkUSJ3ocWVPh6nIIjJS7TLLejIg');
var sh = ss.getSheetByName('Database');
var str = "OK";
sh.getRange(i+1, 2).setValue(str);
}
function doGet() {
var output = HtmlService.createHtmlOutputFromFile('list');
return output;
}
Few more notes:
The two apps are in two different projects in Google Apps Script.
App2 run OK if executed independently
When running App1, I noticed App2 is not executed from my executions logs in Google Apps Script.
I added try-catch code to see if there is an error when executing. It does not look like there is an error.
The reponse has ~70000 characters. I am wondering if that is normal.
The if condition:
if(vA[i][3]+" REQ ID: "+vA[i][0] == value) {
will never be true as anything + "REQ ID: " will never be equal to "TEST"(value). Thus the urlfetch is never executed.

My Script works for some people but not others

I created an audit template in Google sheets that uses several scripts. The script is designed to populate a cell with the active user's email and a time date stamp when they click on a check box. The script works, except for one person, the script does not recognize his active user email. Can someone tell me why the script works for some but not others and how I might fix it?
I've tested the script in a blank test spreadsheet and it works for this person. We had this same issue in another Google Sheet, but we remedied it my creating a new spreadsheet and copying all of the tabs and script. However, attempts to do the same with this spreadsheet have failed.
Here's my script:
function onEdit(e) { //this is the event Google Sheets fires when cells are changed
var email = Session.getActiveUser().getEmail(); // Get the email address of the person running the script.
var date = new Date();
var spreadsheet = SpreadsheetApp.getActive();
if (e.source.getActiveSheet().getName() == "InterimTestsOfControls") { //only do this if the changed cell is on the specific worksheet you care about
switch (e.range.getA1Notation()) { //This gets the cell that was edited
case "E5": //and this switch statement allows you to only respond to the cells you care about
if (e.value == "TRUE") {
SpreadsheetApp.getActiveSheet().getRange("F5").setValue("Prepared by " + email + " " + date);
Logger.log(email);
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Audit Planning'), true).getRange('C7').activate().setValue(email);
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('InterimTestsOfControls'), true).getRange('C5').activate();
}
else {
SpreadsheetApp.getActiveSheet().getRange("F5").setValue("Click box to sign-off as preparer");
Logger.log(email);
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Audit Planning'), true).getRange('C7').activate().setValue("");
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('InterimTestsOfControls'), true).getRange('C5').activate();
}
}
}
}
From the official Session class documentation:
getActiveUser()
Gets information about the current user. If security policies do not allow access to the user's identity, User.getEmail() returns a blank string.
This situation may happen under many, different circumstances:
The user's email address is not available in any context that allows a script to run without that user's authorisation, like a simple onOpen(e) or onEdit(e) trigger, a custom function in Google Sheets, or a web app deployed to "execute as me" (that is, authorized by the developer instead of the user).
These restrictions generally do not apply if the developer runs the script themselves or belongs to the same G Suite domain as the user.
In your situation I see that you are using a simple onEdit(e) trigger - so it looks like this may be the issue. You may want to check out Installable triggers instead.
function onEdit(e) {
var sh=e.range.getSheet();
var email = Session.getActiveUser().getEmail(); //This is a permissions issue. You may have to use an installable trigger and even then it's not guaranteed
var date = new Date();//you might want to use Utilities.formatDate()
//var spreadsheet = SpreadsheetApp.getActive();this is than same as e.source
if (sh.getName()=="InterimTestsOfControls") {
//you can selection your cells with information that's already in the event block rather than having to run another function
if(e.range.columnStart==6 && e.range.rowStart==5) {
if (e.value=="TRUE") {
sh.getRange("F5").setValue("Prepared by " + email + " " + date);
e.source.getSheetByName('Audit Planning').getRange('C7').setValue(email);
} else {
e.source.getRange("F5").setValue("Click box to sign-off as preparer");
e.source.getSheetByName('Audit Planning').getRange('C7').setValue("");
}
}
}
}
Getting and setting the active range is a remnant of the Macro Recorder following your every step and in general is not required in your scripts. And in fact it will slow down your scripts so please try to avoid using that approach in general for most scripting.

Google Sheets Auto-Populate the author for each row on change of column B

How can I add an auto-populate of the user who last edited the row to the following code that auto-populates the date the column was last edited. Here is the current script:
function onEdit(e) {
var s = e.source.getActiveSheet(),
cols = [2],
colStamp = 1,
ind = cols.indexOf(e.range.columnStart)
if (s.getName() !== 'Log' || ind == -1) return;
e.range.offset(0, parseInt(colStamp - cols[ind]))
.setValue(e.value ? new Date() : null);
}
So, ultimately... I would want to add the user name to column G on edit of each row. Any ideas?
You can retrieve user name using Class Session. The detail information is here. https://developers.google.com/apps-script/reference/base/session
Please add following script to last row of your script. The user name is imported to column G. If you want user e-mail, please change from getUsername() to getEmail().
Script :
s.getRange(e.range.getRow(), 7).setValue(Session.getEffectiveUser().getUsername());
About onEdit()
If the onEdit() is not installed as a trigger, other shared users cannot use Session.getEffectiveUser().getUsername() because of authMode=LIMITED. So the user name cannot be retrieved. By installing onEdit() as a trigger, authMode becomes FULL. So you can retrieve user data using Session.getEffectiveUser().getUsername().
The detail information for installing trigger is https://developers.google.com/apps-script/guides/triggers/installable#managing_triggers_manually
If I misunderstand your question, I'm sorry.
Added 1 :
In order to retrieve user information, the user has to install a trigger for onEdit(). I had forgotten about this. I'm sorry.
For example, owner of spreadsheet and user with a permission for editing are OWNER and USER, respectively. When OWNER installs a trigger for onEdit(), the user of spreadsheet becomes OWNER. At this time, when USER edits the spreadsheet, the user name becomes OWNER.
When I have worked a test, I have installed as USER. So I had thought that it works. But it was wrong. So I thought for the solution as follows.
Install a trigger for onEdit() using onOpen().
This didn't work, as you know.
Display a dialog box and a button using onOpen(). Install a trigger for onEdit() by the button.
This didn't work, because of existing several triggers for each user.
Display a dialog box and a button using onOpen(). while temporarily install a trigger for onEdit(), retrieve the user name and put it to cache by the button. The trigger is removed after retrieved user name soon.
This works fine.
I propose the 3rd method. In this script, it is not necessary to install triggers manually. If you want to change the hold time of cache, please modify cache.put().
Script :
function getUser() {
var triggerId = ScriptApp.newTrigger('onEdit')
.forSpreadsheet(SpreadsheetApp.getActive())
.onEdit()
.create().getUniqueId();
var user = Session.getEffectiveUser().getUsername();
var triggers = ScriptApp.getProjectTriggers();
for (i in triggers) {
if (triggers[i].getUniqueId() == triggerId) {
ScriptApp.deleteTrigger(triggers[i]);
}
}
var cache = CacheService.getUserCache();
cache.put("username", user, 3600); // For example, hold user name for 1 h
}
function onOpen() {
SpreadsheetApp.getActiveSpreadsheet().show(
HtmlService
.createHtmlOutput('<input type="button" value="OK" onclick="google.script.run.withSuccessHandler(function(){google.script.host.close()}).getUser()">')
.setTitle('Push OK button.')
.setWidth(400)
.setHeight(100)
);
}
function onEdit(e) {
var cache = CacheService.getUserCache();
var user = cache.get("username"); // Please use this as user name.
}
Flow of script :
When spreadsheet is opened, a dialog is opened on the spreadsheet by onOpen().
When user pushes "ok", the user name is retrieved and put to the cache by getUser().
When user edits the spreadsheet, the user name is retrieved from the cache by onEdit().
Please copy and paste this script. You can use user in onEdit().
When I have been confirming this again, I noticed that in order to use this script, each user has to be authorized. The authorization is https://developers.google.com/apps-script/guides/services/authorization
Added 2 :
I report a solution for retrieving shared user information at spreadsheet. It was found as follows.
User information retrieving by Class Session is the owner and users which installed triggers by themselves.
When each user installs a trigger, user information retrieving by Class Session losts the accuracy. So user information has to be retrieved using a temporally installed trigger.
Using onOpen(), it cannot directly install triggers and authorize.
Using menu bar, it can install triggers and authorize Google Services using API.
Here, I thought 2 problems.
The confirmation whether the authorization was done.
At onOpen(), although many methods using Google API can be executed without the authorization, there are also some methods which cannot be executed without the authorization. Furthermore, there are some methods which cannot execute even if the authorization was done. It's trigger. On the other hand, DriveApp requires the authorization for only the first time, but it can use without the authorization after 2nd times.
I thought that users can find easily by displaying information in a dialog box when spreadsheet is launched. So I adopted displaying information using the dialog box. But, there is a big limitation for the dialog box.
Using a click of button on a dialog box, it can install triggers. However it cannot authorize Google Services using API.
Using above information, I thought a flow to retrieve user information.
When user opens the spreadsheet for the first time, it displays 'Please authorize at "Authorization" of menu bar.' using a dialog box, and creates a menu bar "Authorization".
The user clicks "OK" button on the dialog box and run "Authorization" at the menu bar. By running "Authorization", the user information is retrieved by a temporally installed trigger.
When the user opens the spreadsheet after the 2nd time, the authorization is checked by DriveApp. A dialog box with 'Push OK button.' is displayed. By clicking "OK", the user information is retrieved by a temporally installed trigger.
By this flow, the user information which is using the shared spreadsheet can be retrieved. Although I think that there may be also other solutions, I proposal this as one of solutions.
Script :
function getUser(){
var triggerId = ScriptApp.newTrigger('getUser')
.forSpreadsheet(SpreadsheetApp.getActive())
.onEdit()
.create()
.getUniqueId();
var userInf = Session.getEffectiveUser();
var userName = userInf.getUsername();
var userMail = userInf.getEmail();
var triggers = ScriptApp.getProjectTriggers();
[ScriptApp.deleteTrigger(i) for each (i in triggers) if (i.getUniqueId() == triggerId)];
CacheService.getUserCache().putAll({
"username": userName,
"usermail": userMail
}, 3600);
}
function dialogForGetUser(){
SpreadsheetApp.getActiveSpreadsheet().show(
HtmlService
.createHtmlOutput('<input type="button"\
value="OK"\
onclick="google.script.run.withSuccessHandler(function(){google.script.host.close()})\
.getUser()">'
)
.setTitle('Push OK button.')
.setWidth(400)
.setHeight(100)
);
}
function dialogForAuth(){
SpreadsheetApp.getActiveSpreadsheet().show(
HtmlService
.createHtmlOutput('<input type="button"\
value="OK"\
onclick="google.script.host.close()">'
)
.setTitle('Please authorize at "Authorization" of menu bar.')
.setWidth(400)
.setHeight(100)
);
}
function getAuth() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.removeMenu("Authorization");
getUser();
ss.toast("Done.", "Authorization", 3);
}
function onOpen(){
try {
var temp = DriveApp.getFileById(SpreadsheetApp.getActiveSpreadsheet().getId());
dialogForGetUser();
} catch(e) {
dialogForAuth();
SpreadsheetApp.getActiveSpreadsheet().addMenu(
"Authorization",
[{
functionName:"getAuth",
name:"Run this only when the first time"
}]
);
}
}
function onEdit(e){
var cache = CacheService.getUserCache();
var user = cache.getAll(["username", "usermail"]);
// user.username is user name.
}
When the spreadsheet is opened, at first, onOpen() is executed. It is checked whether the user has already authorized.
If the user has never authorized yet, dialogForAuth() is executed. If the user has already authorized. dialogForGetUser() is executed.
In this case, you can retrieve user name by user.username at onEdit().

Use Google Script's Web App as Webhook to receive Push Notification directly

My Goal: Changes in Google Drive => Push Notification to https://script.google.com/a/macros/my-domain/... => App is pushed to take action.
I don't want to setup an middle Webhook agent for receiving notification. Instead, let the Web App (by Google Script) to receive it and be pushed directly.
Since the relevant function is quite undocumented (just here: https://developers.google.com/drive/web/push) , below is the code I tried but failure.
1. Is above idea feasible??
2. My code doPost(R) seems cannot receive notification (R parameter) properly. Anyway, no response after I change the Google Drive. Any problem? (I have tried to log the input parameter R so as to see its real structure and decide if the parameter Obj for OAuth is the same as normal Drive App, but error occur before log)
function SetWatchByOnce(){
var Channel = {
'address': 'https://script.google.com/a/macros/my-domain/.../exec',
'type': 'web_hook',
'id': 'my-UUID'
};
var Result = Drive.Changes.watch(Channel);
...
}
function doPost(R) {
var SysEmail = "My Email";
MailApp.sendEmail(SysEmail, 'Testing ', 'Successfully to received Push Notification');
var Response = JSON.parse(R.parameters);
if (Response.kind == "drive#add") {
var FileId = Response.fileId;
MyFile = DriveApp.getFolderById(FileId);
...
}
}
function doGet(e) {
var HTMLToOutput;
var SysEmail = "My Email";
if (e.parameters.kind) {
//I think this part is not needed, since Push Notification by Drive is via Post, not Get. I should use onPost() to receive it. Right?
} else if (e.parameters.code) {
getAndStoreAccessToken(e.parameters.code);
HTMLToOutput = '<html><h1>App is successfully installed.</h1></html>';
} else { //we are starting from scratch or resetting
HTMLToOutput = "<html><h1>Install this App now...!</h1><a href='" + getURLForAuthorization() + "'>click here to start</a></html>";
}
return HtmlService.createHtmlOutput(HTMLToOutput);
}
....
Cloud Functions HTTP trigger(s) might also be an option ...
(which not yet existed at time of this question). this just requires setting the trigger URL as the notification URL, in the Google Drive settings - and adding some NodeJS code for the trigger; whatever it shall do. one can eg. send emails and/or FCM push notifications alike that. that trigger could also be triggered from App Script, with UrlFetchApp and there is the App Script API. one can have several triggers, which are performing different tasks (App Script is only one possibilty).
Cicada,
We have done similar functions to receive webhooks/API calls many times. Notes:
to get R, you need: var Response = R.parameters and then you can do Response.kind, Response.id, etc.
Logger will not work with doGet() and doPost(). I set it up a write to spreadsheet -- before any serious code. That way I know if it is getting triggered.

Using Cache from Server Handler

I am trying to save some user input from a Google App Script form on a spreadsheet to the private cache.
Here is a test script:
var cache = CacheService.getPrivateCache();
function onLoad() {
var sheet = SpreadsheetApp.getActiveSpreadsheet(),
entries = [{
name : "Show form",
functionName : "showForm"
}];
sheet.addMenu("Test Menu", entries);
}
function showForm() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet(),
app = UiApp.createApplication(),
setButton = app.createButton("Set"),
setHandler = app.createServerClickHandler('setTest');
setButton.addClickHandler(setHandler);
app.add(setButton);
spreadsheet.show(app);
}
function setTest(event) {
cache.put("test", "test", 7200);
Browser.msgBox("Test was set: " + cache.get("test") + ". Use the getTest cell formula to test the cache.");
}
function getTest() {
var result = cache.get("test");
return result;
}
Once I click the menu button, a form appears and sets a cache value in a server handler. Then I try to get the value from the cache using =getTest() in a cell. I would expect the cache to return the value "test" but it seems to return null.
I started this issue here:
http://code.google.com/p/google-apps-script-issues/issues/detail?id=2039
And I also found another similar one:
http://code.google.com/p/google-apps-script-issues/issues/detail?id=1804
Trying to save some user input on a form into the cache and be able to access it from another function at a later time.
Any suggestions?
Custom functions have limited scope and then can only access a few select services. You read more about their limitations here.
With Cache its a bit tricky - caching is per "script scope". So when a Cache is accessed first from the Server handler script scope its running as a UiApp which has more privileges and that is different from the script scope that a custom function runs as. Therefore, the cache is not shared between those two scopes.
You can access cached items that you set in custom functions from other custom functions, but this cache can't cross these boundaries.
You could theoretically store this in ScripProperties but that can very clumsy and it would be misusing those capabilities and this is shared amongst all users.
ScriptProperties.setProperty("test", "test")
and
var result = ScriptProperties.getProperty("test");
If you can explain your usecase a bit more in depth, perhaps we can provide some alternative solutions.