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.
Related
I have a spreadsheet I'm using to manage a bunch of content, with a script I've written that adds an "Export" button to the menu. When the button is clicked, the script gets all the appropriate data and formats it all in a specific way. The formatted version is saved to my Google Drive with a timestamp but a download link is also provided. I'll include a simplified version of the script below in case modifications are required.
I rarely ever use Google's Apps Scripts so I'm rather unfamiliar with the ins and outs of it. I only know the basics (how to write a script that can run when something is done from the spreadsheet's page).
I'm aware I can invite a user to my spreadsheet (or just make it public) but that doesn't seem to bring the script along with it. The script and all the formatting that's being done is the main part of what the person I'm inviting needs. I'm aware that for file.getDownloadUrl() to work (assuming the file is still saving on my Drive), I'd need to give the individual access to that folder as well which isn't a problem.
The question is, how do I give them access to the script so they get the Export menu item? Am I not able to? Am I basically limited to creating a button with the export function bound to it?
function onOpen() {
var ss = SpreadsheetApp.getActiveSpreadsheet()
var csvMenuEntries = [
{
name: "Export as CSV",
functionName: "csvExport"
},
{
name: "Export for wiki",
functionName: "wikiExport"
}
]
ss.addMenu("Export", csvMenuEntries)
}
function prepare(type) {
const ss = SpreadsheetApp.getActiveSpreadsheet()
const ssName = ss.getName()
const sheet = ss.getSheets()[0]
const sheetName = sheet.getSheetName()
const folderName = ssName + ' exports'
let folder
try {
folder = DriveApp.getFoldersByName(folderName).next()
} catch (err) {
folder = DriveApp.createFolder(folderName)
}
let fileName
if (type) {
const extension = type === 'csv' ? 'csv' : 'txt'
fileName = ssName + '_' + sheetName + `_${type}_` + new Date().getTime() + `.${extension}`
}
return { ss, ssName, sheet, sheetName, folder, fileName }
}
function download(file) {
const downloadURL = file.getDownloadUrl().slice(0, -8)
showUrl(downloadURL)
}
function showUrl(downloadURL) {
var link = HtmlService.createHtmlOutput(`Click here to download`)
SpreadsheetApp.getUi().showModalDialog(link, 'Your file is ready!')
}
function csvExport() {
const { ss, sheet, folder, fileName } = prepare('csv')
const csvSettings = getCsvSettings(ss)
const csvFile = convertRangeToCsv(sheet, csvSettings) // not going to share this. It's simple but irrelevant
const file = folder.createFile(fileName, csvFile)
download(file)
}
function wikiExport() {
const { sheet, folder, fileName } = prepare('wiki')
const wikiFile = convertRangeToWikiFormat(sheet) // not going to share this. It's simple but irrelevant
const file = folder.createFile(fileName, wikiFile)
download(file)
}
A container-bound script has the same access as its parent spreadsheet, so if you're sharing the spreadsheet you're also sharing the script (though if they have only view access they have to create their own copy to see it):
All container-bound scripts use the same owner, viewer, and editor access list defined for the container file.
With that in mind, there are a few limitations when using scripts. First, they will not trigger for anonymous users (i.e., users that are not signed in), even if the sheet is editable to the public. You'll notice that if you try to open the script editor as anonymous, you will be asked to sign in. There's also a feature request to allow this on Google's issue tracker here.
Secondly, even if the users are signed in, there are other restrictions for Apps Script's triggers:
onOpen(e) runs when a user opens a spreadsheet, document, presentation, or form that the user has permission to edit.
Users need permission to edit the file for the onOpen() trigger to run. If they have viewer or commenter access the menu won't show up. In fact, you'll find that most script functions won't work if the users have only viewer access since they need editor access for most interactions with the sheet.
So if you want this menu to show up you'll need to give your users explicit editor access. If you really must keep your sheet as view-only or want to interact with anonymous users you can consider building a Web App instead and have the users get the download link from there. The web app has ways to communicate with the back-end or the Sheet so you should be able to reproduce your current code that way as well.
References:
Web Apps
Communicating with server functions
Triggers
I am currently writing a Google Apps Script inside a Google Sheet to read data from a list of spreadsheets (spreadsheet url is provided by the user). However, I cant seems to find a way to check if the url is valid or if user have access to the spreadsheet or not before calling SpreadsheetApp.openByUrl().
I have written the following code to "validate" the url:
for(int i = 0; i < urls.length; i++) {
let spreadsheet = null
try {
spreadsheet = SpreadsheetApp.openByUrl(urls[i]);
} catch (e) {
continue;
}
// Continue to do other stuff to read data from spreadsheet...
}
This however has an issue, it was able to catch the first few 'You do not have permission to access the requested document.' exception. But after a certain number of exception had occur, I would get a permenant error that cant be caught, stopping the script all together.
Is there a better way to do this?
Minimal reproducible example:
Create 3 google sheet using different google account
Using a different google account, create a google sheet and add the following code into Code.gs
function myFunction() {
// Put any 3 real spreadsheet url that you do not have access to
let urls = [
"https://docs.google.com/spreadsheets/d/1gOyEAz0amm4RghpE4B7f26okU3PG3vWZkrfiC-SBlbw/edit#gid=0",
"https://docs.google.com/spreadsheets/d/1Oia7ADu5BmYroUq1SLyDMHTJowrwSXOhCEyNO3nXmMA/edit#gid=0",
"https://docs.google.com/spreadsheets/d/1HE_IXURpBr_FJN--mwLo6k9gih07ZEtDGBqYSk6KgiA/edit#gid=0",
]
urls.forEach(url => {
try {
SpreadsheetApp.openByUrl(url)
} catch (e) {
console.log("Unable to open this spreadsheet")
}
})
}
function onOpen() {
SpreadsheetApp.getUi().createMenu("Test").addItem("myFunction", "myFunction").addToUi()
}
Run the function once in the apps script panel and authorize the application
Refresh this google sheet
Wait for the Custom Menus to show up and press "Menu" > "myFunction"
As you can see, the openByUrl() call is sitting inside the try catch block, however when you run the function through custom menu, you will still get "Error: You do not have permission to access the requested document.".
Executions Log:
From your question, I thought that your situation might be due to the specification or a bug of SpreadsheetApp.openByUrl. If my understanding is correct, in order to avoid this issue, how about putting the method for checking whether the file can be used before SpreadsheetApp? In your script, how about the following modification?
From :
SpreadsheetApp.openByUrl(url)
To:
var fileId = url.split("/")[5];
var file = DriveApp.getFileById(fileId);
spreadsheet = SpreadsheetApp.open(file);
In this modification, the file is retrieved with DriveApp.getFileById(fileId). When fileId cannot be used, an error occurs. But in this case, try-catch can be correctly worked. By this, the issue of SpreadsheetApp doesn't occur.
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.
I have a function launchForm within a Google Sheet which calls another function createForm with a single question. I get the URL of the newly created form and pass it into a sandbox Iframe using UrlFetchApp, as shown below:
function launchForm() {
var form = createForm(); // separate function that works fine
formUrl = form.getPublishedUrl()
var response = UrlFetchApp.fetch(formUrl,{"followRedirects" : true}); // true if automatic redirecting works
var formHtml = response.getContentText();
var htmlApp = HtmlService
.createHtmlOutput(formHtml)
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) // did this to prevent error but didn't help
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.show(htmlApp);
}
I have created a custom menu in sheets to run the launchForm function.
The trouble is, that the new form URL is at docs.google.com/a/, while the sheet is at docs.google.com/spreadsheets/d/...
This means that the iframe, instead of loading the form, opens up to a sign-in screen in when I (the creator of the form) tries to run it. When someone else with edit permissions runs the script, he sees the form as expected, but is then unable to make any inputs into the form.
I have read about CORS, X-frame-options, OAuth2, and nothing seems to provide the precise answer about what I must do. Any help will be much appreciated.
I am testing the functionality of UrlFetchApp and passing data from a Form and its Spreadsheet. I know it's possible to do this another way, however I am testing the functionality of UrlFetchApp (first time using it) within google scripts themselves, and want to get it to work with this method.
Here's the scenario I got, add a bound script to a Form App as so:
function makeRequest()
{
var webAppUrl = "https://script.google.com/macros/s/WebAppID/exec";
var auth = ScriptApp.getOAuthToken();
var header = { 'Authorization': 'Bearer ' + auth };
var options = { 'method':'post', 'headers':header };
var resp = UrlFetchApp.fetch(webAppUrl, options);
Logger.log(resp);
}
Add a bound script to the attached spreadsheet:
function doPost()
{
var ss = SpreadsheetApp.openById('ssID');
var name = ss.getName();
return ContentService.createTextOutput(name);
}
And then publish this second script attached to the sheet as a web app with only myself to have access.
Currently the above code does not work. The following error appears on the Form side of the script:
Request failed for
https://script.google.com/macros/s/WebAppID/exec
returned code 401. Truncated server response:
Unauthorized Unauthorized Error 401
(use muteHttpExceptions option to examine full response) (line
12, file "Code")
Fails on the UrlFetchApp line of code.
However, if I remove the header option, then publish the web app for public use, this works just fine. Obviously this is not wanted.
What am I missing regarding authentication between scripts that I own?
Side Notes:
Anyone know why SpreadsheetApp.getActiveSheet() doesn't work when run in this fashion? That script is directly bound to a google sheet, so kind of odd.
Ok, found the answer to my own question. It was quite simple really. Needed to add the following scope to my project for accessing a spreadsheet:
https://www.googleapis.com/auth/drive
The easiest way I found to do this is to add a simple function like this and call it:
function authorizeDrive()
{
var forScope = DriveApp.getRootFolder();
}
Doesn't need to return or do anything, just call any method from the DriveApp. Once run, it'll then popup with a dialogue for authorization. Don't even need to call this every time you do your main method calls. Don't even need to leave it coded in the script either. I wonder if there is way to just simple add the scope you need to a project from a properties window (I didn't find any). Or perhaps a way to pass a parameter along with UrlFetchApp regarding what scope need authorized.
Buy anyhow this still wasn't too bad.
Regarding my side note, I still haven't found a reason as to why SpeadsheetApp.getActiveSheet() returns null or undefined. I have to open by ID or URL, which is a pain. Especially since this is a container bound script. Also noticed that Logger.log() doesn't actually add anything to the Logger when run in this manner. If anyone could still shed some light on either of these, that would be great.
You need to get the 'Spreadsheet' object first.
SpeadsheetApp.getActive().getActiveSheet()
However, if you are creating an add-on menu you can use 'SpreadsheetApp.getActiveSheet()'
function myFunction() {
var lastRow = SpreadsheetApp.getActiveSheet().getLastRow();
var range = SpreadsheetApp.getActiveSheet().getRange(lastRow, 1, 1, 26);
SpreadsheetApp.setActiveRange(range);
}
function onOpen(e) {
SpreadsheetApp.getUi().createAddonMenu()
.addItem('showLastRow', 'myFunction')
.addToUi();
}