How to run certain scripts under the authority of a specific user? - javascript

I was able to allow other users to add a new SKU to a sheet without unprotecting it (Original post). Now I am trying to do the inverse, to allow users to delete an SKU without unprotecting the sheet.
I started with the following, which works as expected:
function deleteEachRow(){
const ss = SpreadsheetApp.getActive();
var SHEET = ss.getSheetByName("Ordering");
var RANGE = SHEET.getDataRange();
const ui = SpreadsheetApp.getUi();
const response = ui.prompt('WARNING: \r\n \r\n Ensure the following sheets DO NOT contain data before proceeding: \r\n \r\n Accessory INV \r\n Apparel INV \r\n Pending TOs \r\n \r\n Enter New SKU:', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() === ui.Button.OK) {
const text = response.getResponseText();
var rangeVals = RANGE.getValues();
//Reverse the 'for' loop.
for(var i = rangeVals.length-1; i >= 0; i--){
if(rangeVals[i][0] === text){
I tried to Frankenstein the above code into the answer I was provided. Now the script runs without error but fails to delete the entered SKU as expected. This is the script I am running:
function deleteEachRow1(){
const ss = SpreadsheetApp.getActive();
var SHEET = ss.getSheetByName("Ordering");
var RANGE = SHEET.getDataRange();
const ui = SpreadsheetApp.getUi();
const response = ui.prompt('WARNING: \r\n \r\n Ensure the following sheets DO NOT contain data before proceeding: \r\n \r\n Accessory INV \r\n Apparel INV \r\n Pending TOs \r\n \r\n Delete Which SKU?:', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() === ui.Button.OK) {
const text = response.getResponseText();
const webAppsUrl = "WEB APP URL"; // Pleas set your Web Apps URL.
const url = webAppsUrl + "?text=" + text;
const res = UrlFetchApp.fetch(url, {muteHttpExceptions: true});
// ui.alert(res.getContentText()); // You can see the response value using this line.
function doGet(e) {
const text = e.parameter.text;
const sheet = SpreadsheetApp.getActive().getSheetByName('Ordering');
var rangeVals = RANGE.getValues();
//Reverse the 'for' loop.
for(var i = rangeVals.length-1; i >= 0; i--){
if(rangeVals[i][0] === text){
return ContentService.createTextOutput(text);
// This script is from
function a1notation2gridrange1(a1notation) {
var data = a1notation.match(/(^.+)!(.+):(.+$)/);
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(data[1]);
var range = ss.getRange(data[2] + ":" + data[3]);
var gridRange = {
sheetId: ss.getSheetId(),
startRowIndex: range.getRow() - 1,
endRowIndex: range.getRow() - 1 + range.getNumRows(),
startColumnIndex: range.getColumn() - 1,
endColumnIndex: range.getColumn() - 1 + range.getNumColumns(),
if (!data[2].match(/[0-9]/)) delete gridRange.startRowIndex;
if (!data[3].match(/[0-9]/)) delete gridRange.endRowIndex;
return gridRange;
// Please run this function.
function myFunction() {
const email = "MY EMAIL"; // <--- Please set your email address.
// Please set your sheet names and unprotected ranges you want to use.
const obj = [
{ sheetName: "Ordering", unprotectedRanges: ["O5:P", "C2:E2"] },
{ sheetName: "Accessory INV", unprotectedRanges: ["E5:H"] },
{ sheetName: "Apparel INV", unprotectedRanges: ["E5:F"] },
{sheetName: "Pending TOs", unprotectedRanges: ["E6:H"] },
{sheetName: "INV REF", unprotectedRanges: ["C6:C"] },
// 1. Retrieve sheet IDs and protected range IDs.
const spreadsheetId = SpreadsheetApp.getActiveSpreadsheet().getId();
const sheets = Sheets.Spreadsheets.get(spreadsheetId, { ranges:{ sheetName }) => sheetName), fields: "sheets(protectedRanges(protectedRangeId),properties(sheetId))" }).sheets;
const { protectedRangeIds, sheetIds } = sheets.reduce((o, { protectedRanges, properties: { sheetId } }) => {
if (protectedRanges && protectedRanges.length > 0) o.protectedRangeIds.push({ protectedRangeId }) => protectedRangeId));
return o;
}, { protectedRangeIds: [], sheetIds: [] });
// 2. Convert A1Notation to Gridrange.
const gridranges ={ sheetName, unprotectedRanges }, i) => => a1notation2gridrange1(`${sheetName}!${f}`)));
// 3. Create request body.
const deleteProptectedRanges = protectedRangeIds.flatMap(e => => ({ deleteProtectedRange: { protectedRangeId: id } })));
const protects =, i) => ({ addProtectedRange: { protectedRange: { editors: {users: [email]}, range: { sheetId }, unprotectedRanges: gridranges[i] } } }));
// 4. Request to Sheets API with the created request body.
Sheets.Spreadsheets.batchUpdate({ requests: [...deleteProptectedRanges, ...protects] }, spreadsheetId);

Probably the easiest way to do this would be to avoid using a button and using a checkbox with a installable edit trigger, which also has a great side effect of mobile support.
Proposed solution:
Using a checkbox
Hook it to a installable edit trigger, which runs as the user who installed the trigger. Therefore, if the owner installs the trigger, no matter who edits the sheet, the trigger runs as the owner, giving access to privileged resources including protected ranges.
The installable version runs with the authorization of the user who created the trigger, even if another user with edit access opens the spreadsheet.
Code simplicity and maintainabilty. No need for webapp or any complicated setup.
Disadvantage: Security (with possible workaround)
If the code is bound to the sheet, editors of the sheet get direct access to the script of the sheet. So, any editor with malicious intentions would be able to modify the code. If the function with installable trigger has gmail permissions, any editor would be able to log all the emails of the owner. So,special attention needs to be paid to permissions requested. Note that, this is already the case with your web app setup. Any editor maybe able to modify doGet to access protected data. If the webapp is in a separate standalone script, this isn't a issue. You may also be able to fix this issue by setting the trigger at a predetermined version instead of Head version. See this answer for more information.


Get File Name from list of URL's - Google Drive

So I'm needing to get the list of file names from a range of Google Drive URLs in a spreadsheet. Browsing around the net, I came across the code below. It works but only for the old style urls, which I heard Google changed in September 2021.
Note that links are not fully functional, please replace with real links to check!
The old style is:
This works correctly from the code below.
What I'd like though is two things.
It should handle a range of a couple of columns, currently reading AE2:AE, and printing out on AM2:AM. What I'd like is to go through the range: AE2:AL and print out: AM2:AT
Secondly it should also handle the newer form urls:
Current Code:
function getNames() {
var activeRange = SpreadsheetApp.getActiveSheet().getDataRange();
var height = activeRange.getHeight();
var links = SpreadsheetApp.getActiveSheet()
.getRange("AE2:AE" + height)
var nameValues = [];
links.forEach((row) => {
try {
var link = row[0];
var fileID = getIdFromLink(link);
var name = DriveApp.getFileById(fileID).getName();
} catch (e) {
nameValues.push(["NO NAME FOUND"]);
var nameRange = SpreadsheetApp.getActiveSheet().getRange("AM2:AM" + height);
function getIdFromLink(link) {
var regex = new RegExp(
return regex.exec(link)[0];
How should the code above be modified to enable what I'm wanting. Sorry, I tried a couple of if/else statements, but my Javascript knowledge is severely limited.
Any help would be greatly appreciated.
Current "screenshot" showing:
(1) - Old style url - correctly picking up file name (2)
(3) - New style url - not picking up file name (4)
Your getIdFromLink() function should work just fine as long as the files have not been shared in such a way that they require a resource key as well.
To work with resource keys, use DriveApp.getFileByIdAndResourceKey(), like this:
function getFileNamesByLink() {
const sheet = SpreadsheetApp.getActiveSheet();
const sourceRange = sheet.getRange('AE2:AL');
const targetRange = sheet.getRange('AM2');
const fileNames = sourceRange.getValues()
.map(row => => getFileNameFromLink_(link)));
.offset(0, 0, fileNames.length, fileNames[0].length)
function getFileNameFromLink_(link) {
if (!link) {
return null;
const fileId = getIdFromLink_(link);
if (!fileId) {
return NaN;
let file;
try {
file = DriveApp.getFileById(fileId);
} catch (error) {
try {
file = DriveApp.getFileByIdAndResourceKey(fileId, getResourceKeyFromLink_(link));
} catch (error) {
return NaN;
return file.getName();
function getIdFromLink_(link) {
const match = String(link).match(/file\/d\/([-\w]+)/i);
return match ? match[1] : null;
function getResourceKeyFromLink_(link) {
const match = String(link).match(/resourcekey=([-\w]+)/i);
return match ? match[1] : null;
Note that the script may time out if you have thousands of links. If that happens, process the links in a piecemeal fashion, or see if the Advanced Drive Service works for you.

Google Sheets Scripts - run scripts as administrator / owner

I have Google Sheet, name TEST
Sheet: Arkusz 1
Column A: all people can edit
Column B: only owner can edit
Library (for everyone):
A user cannot add a row because it is blocked by column B, which belongs only to the admin.
How can I create macro, which allow user to add new rows?
I have three scripts:
function insertRow() {
var ss = SpreadsheetApp.getActive()
var sheetName = ss.getActiveSheet().getName()
var row = ss.getActiveRange().getRow()
var numRows = Browser.inputBox('Insert Rows', 'Enter the number of rows to insert', Browser.Buttons.OK);
var url =""
var queryString = "?sheetName="+sheetName+"&rowNo="+row+"&noOfRows="+numRows
url = url + queryString
var request = UrlFetchApp.fetch(url)
if (request != 'Success')
function doGet(e) {
var param = e.queryString
var parameters = param.split("&")
// This just checks only 3 parameters are present else gives a invalid link
if (param != null && parameters.length == 3){
param = e.parameter
var name = param.sheetName
var row = Number(param.rowNo)
var numOfRows = Number(param.noOfRows)
} else{
return ContentService.createTextOutput("Invalid query")
var ss = SpreadsheetApp.openById("")
var sheet = ss.getSheetByName(name)
sheet.insertRowsAfter(row, numOfRows);
var source_range = sheet.getRange(row,1,1,sheet.getLastColumn());
var target_range = sheet.getRange(row+1,1,numOfRows);
catch (err){
return ContentService.createTextOutput("error: "+err)
return ContentService.createTextOutput("Success")
And after clicked function insertRow and filled number of rows I have doPost(e) information.
Could you help me?
On the solution you provided below, I see that the issue is in mainScript
function mainScript(e) {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
// assign the sheet to a variable and use it below instead of spreadsheet
var sheet = spreadsheet.getSheetByName('ZNC')
sheet.insertRowsBefore(sheet.getActiveRange().getRow(), 1);
Hmm, I created solution, but I think there's a bug somewhere, because it doesn't add the line, even though everything is correct and the script is published as public.
function ZNCWiersz() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('ZNC'), true);
const activeSheet = SpreadsheetApp.getActiveSheet().getSheetName();
const url = ScriptApp.getService().getUrl();
UrlFetchApp.fetch(`${url}?sheetName=${activeSheet}`, {
headers: { authorization: "Bearer " + ScriptApp.getOAuthToken() },
// DriveApp.getFiles() // This is used for automatically detecting the scope of "". This scope is used for the access token.
// When runScript() is run, this function is run.
const doGet = (e) => ContentService.createTextOutput(mainScript(e));
// This script is run by Web Apps.
function mainScript(e) {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
spreadsheet.insertRowsBefore(spreadsheet.getActiveRange().getRow(), 1);

How to batch row data and send a single JSON payload?

I currently use a Google Apps Script on a Google Sheet, that sends individual row data to AWS API Gateway to generate a screenshot. At the moment, multiple single JSON payload requests are causing some Lambda function failures. So I want to batch the row data and then send as a single payload, so a single AWS Lambda function can then perform and complete multiple screenshots.
How can I batch the JSON payload after iterating the data on each line in the code below?
function S3payload () {
var sheet = SpreadsheetApp.getActiveSheet(); // Use data from the active sheet
// Add temporary column header for Payload Status new column entries
sheet.getCurrentCell().setValue('payload status');
var startRow = 2; // First row of data to process
var numRows = sheet.getLastRow() - 1; // Number of rows to process
var lastColumn = sheet.getLastColumn(); // Last column
var dataRange = sheet.getRange(startRow, 1, numRows, lastColumn) // Fetch the data range of the active sheet
var data = dataRange.getValues(); // Fetch values for each row in the range
// Work through each row in the spreadsheet
for (var i = 0; i < data.length; ++i) {
var row = data[i];
// Assign each row a variable
var index = row[0]; // Col A: Index Sequence Number
var img = row[1]; // Col B: Image Row
var url = row[2]; // Col C: URL Row
var payloadStatus = row[lastColumn - 1]; // Col E: Payload Status (has the payload been sent)
var siteOwner = "";
// Prevent from sending payload duplicates
if (payloadStatus !== PAYLOAD_SENT) {
/* Forward the Contact Form submission to the owner of the site
var emailAddress = siteOwner;
var subject = "New contact form submission: " + name;
var message = message;*/
//Send payload body to AWS API GATEWAY
//var sheetid = SpreadsheetApp.getActiveSpreadsheet().getId(); // get the actual id
//var companyname = SpreadsheetApp.getActiveSpreadsheet().getName(); // get the name of the sheet (companyname)
var payload = {
"img": img,
"url": url
var url = '';
var options = {
'method': 'post',
'payload': JSON.stringify(payload)
var response = UrlFetchApp.fetch(url,options);
sheet.getRange(startRow + i, lastColumn).setValue(PAYLOAD_SENT); // Update the last column with "PAYLOAD_SENT"
SpreadsheetApp.flush(); // Make sure the last cell is updated right away
// Remove temporary column header for Payload Status
sheet.getCurrentCell().clear({contentsOnly: true, skipFilteredRows: true});
Example individual JSON payload
Example desired output result
Do not call UrlFetchApp methods in a loop unless no other way. Although Google offers generous quota, it is not unlimited, and you will quickly burn through it on any substantial amount of rows and send frequency.
Use modern ES6 features like map to convert rows of values into objects in the format of the desired payload. Note that you will have to enable V8 runtime to use them.
What follows is a runnable test snippet showcasing how you could have modified your script. I opted to exclude status update logic from it, as it is up to you to decide how to update the status in case of batch update failure:
const SpreadsheetApp = {
getActiveSheet() {
const Sheet = {
getLastRow() { return 3; },
getLastColumn() { return 5; },
getDataRange() {
const Range = {
getValues() {
return new Array(Sheet.getLastRow())
(r,ri) => new Array(Sheet.getLastColumn())
.fill(`mock row ${ri}`)
.map((c,ci) => `${c} cell ${ci}`)
return Range;
return Sheet;
const UrlFetchApp = {
fetch(uri, options) {
console.log({ uri, options });
const sendToS3 = () => {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2;
var numRows = sheet.getLastRow() - 1;
var lastColumn = sheet.getLastColumn();
var dataRange = sheet.getDataRange();
var data = dataRange.getValues();
var siteOwner = "";
const appURI = '';
const payloads =[index, img, url]) => ({ img, url }));
const options = {
'method': 'post',
'payload': JSON.stringify(payloads)
const response = UrlFetchApp.fetch(appURI, options);
When batching POST payloads, keep in mind that there is a quota on
maximum body size per request (currently 50 MB).
Do not call I/O (input/output) methods such as getRange, getValue in a loop, they are slow by nature, use batch methods like getDataRange, getValues, setValues, etc and perform all modifications on in-memory arrays only.
Use activate methods only when you explicitly want to change the focus, do not rely on it to determine a range. Just use normal references to cells obtained through methods like getRange.
Try sending the data as a list/Array. And on the server side iterate over the list/Array.
"payload": [{
"img": "",
"url": ""
}, {
"img": "",
"url": ""

How to get active user email with installabled onEdit trigger? [duplicate]

This question already has an answer here:
onEdit trigger doesn't catch current user
(1 answer)
Closed 3 months ago.
I have a Google spreadsheet with some data. I wrote script to track changes of some specific columns.
function onOpen() {
var ss = SpreadsheetApp.getActive();
var menuItems = [
{name: 'Turn on', functionName: 'createSpreadsheetEditTrigger'}
ss.addMenu('Tracker', menuItems);
function changeTrack(){
const ss = SpreadsheetApp.getActiveSpreadsheet();
const ui = SpreadsheetApp.getUi();
var ws = ss.getActiveSheet();
const headerRow = 4;
const editBodyCols = [2, 3, 4, 5];
const fResultCol = 6;
var range = ws.getActiveRange();
var row = range.getRow();
var col = range.getColumn();
let target1 = ws.getRange(row, fResultCol);
let target2 = ws.getRange(row, fResultCol + 1)
let activeUser = getCurrentUserEmail();
if(row > headerRow && editBodyCols.some(x => x === col) === true){
if(target1.getValue() !== ""){
target2.setValue(result(ss, ws, row, activeUser)[1]);
} else {
target1.setValue(result(ss, ws, row, activeUser)[0])
target2.setValue(result(ss, ws, row, activeUser)[1])
function createSpreadsheetEditTrigger() {
var ss = SpreadsheetApp.getActive();
function date(){
return Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss");
function result(ss, ws, row, activeUser) {
const ssName = ss.getName();
let data = `Создал ${activeUser} ${date()}`;
let exp = `Файл ${ssName}, Лист ${ws.getName()}, изменил ${activeUser}, строка № ${row}, ${date()}`;
let adds = [];
return adds;
function getCurrentUserEmail()
var email=Session.getActiveUser().getEmail();
return email;
My problem is to get active user's email. This script can get it but not all the time. Seems like random success. It means sometimes I can get expected value, sometimes not. I don't understand what is it depends from.
Where I'm wrong and how to fix it?
From the documentation on Session.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. The circumstances in which the email address is available vary: for example, the user's email address is not available in any context that allows a script to run without that user's authorization, 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).
So this seems pretty expected and there is no hard workaround you can make to retrieve the users mail. You should maybe just ask for it and see if they be willingly want to give it to you.
Although if you are the developer or the users are inside your organization this restrictions may be ignored:
However, these restrictions generally do not apply if the developer runs the script themselves or belongs to the same G Suite domain as the user.
Based on the comment by b-frid.
The flow:
create a custom menu and tell each user to run the function twice. First time for authorization, and the second time to actually run the code.
this will install the same trigger with the user's privileges and let the onEdit trigger get the email of an active user (author of the trigger)
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('😎📬 run me 2x times')
.addItem('please let script to see your email', 'install')
// function test() {
// var e = {
// range: SpreadsheetApp.getActive().getSheetByName('test_onEdit').getRange('B2')
// }
// edit_(e)
// }
function edit_(e) {
var targetcol = 1;
if (e.range.getSheet().getName() === 'Sheet1') {
var user = Session.getActiveUser().getEmail();
if (user !== '') {
var col = e.range.getColumn();
var rows = e.range.getHeight();
var ratgetrange = e.range.offset(
targetcol - col,
function install() {
* create onEdit trigger
* #param {string} spreadsheetId
* #param {string} functionName
function setOnEditTrigger_(spreadsheetId, functionName) {
console.log('OnEdit trigger ' + functionName +
' for new file' +
var trigger;
if (existsOnEditTrigger_(functionName, spreadsheetId)) {
console.log('stopped execution. Trigger exists.');
trigger = ScriptApp
console.log('Created new trigger!')
return trigger.getUniqueId();
* check if onEdit trigger exists
* #param {string} spreadsheetId
* #param {string} functionName
function existsOnEditTrigger_(functionName, spreadsheetId) {
var triggers = ScriptApp.getProjectTriggers();
var trigger = {};
for (var i = 0; i < triggers.length; i++) {
trigger = triggers[i];
if (
trigger.getHandlerFunction() === functionName &&
trigger.getTriggerSourceId() === spreadsheetId &&
trigger.getEventType().toString() === 'ON_EDIT') return true;
return false;
Original comment:
The key is separate triggers for each user. Then I simply added code
to execute the update only if the Session.getActiveUser().getEmail()
call does not come back blank. Of course, because each's user's
trigger will run, the function will execute x times where x = the
number of users (i.e. triggers), but with the check for a blank return
value the logic only runs once (and so no overwrites). A bit clumsy
indeed, and perhaps not practical if you have more than a handful of
users, but workable in my case.

How to add Emails as Editors of whole document using a Setup Sheet?

I'm trying to take my project from a stage to another, and I was able to make some good progress so far.
I've got the following script that runs when the sheet called Setup_Protections is edited: it removes all the sheets protections then add them back with the Emails specified in the Setup sheet (i.e. add those emails as editors of the protected sheets).
But the problem is that the spreadsheet needs to be shared beforehand so they can access it first. Is there a way to share in the same time the document with the emails entered in the Setup sheet ? (without necessary using a method that requires enabling Sheets API as I'll be duplicating many times the documents)
Thank you for your help
var environment = {
protectionConfigSheetName: "Setup_Protection",
// Script fires when Setup_Protection is edited
function onEdit(e) {
if (e.range.getSheet().getName() === environment.protectionConfigSheetName)
function removeSpreadsheetProtections(spreadsheet) {
].forEach(function (type) {
return spreadsheet.getProtections(type).forEach(function (protection) { return protection.remove(); });
function getProtectionConfig(spreadsheet) {
var protectionConfigSheetName = "Setup_Protection";
var sheet = spreadsheet.getSheetByName(environment.protectionConfigSheetName);
var values = sheet.getDataRange().getValues();
var protectionConfig = values
.reduce(function (protectionConfig, _a) {
var targetSheetName = _a[0], emailAddress = _a[1];
var config = protectionConfig.find(function (_a) {
var sheetName = _a.sheetName;
return sheetName === targetSheetName;
var editors = emailAddress.split(",");
if (config)
config.editors = config.editors.concat(editors);
sheetName: targetSheetName,
editors: editors.slice()
return protectionConfig;
}, []);
return protectionConfig;
function setSpreadsheetProtections(spreadsheet, protectionConfig) {
spreadsheet.getSheets().forEach(function (sheet) {
var protection = sheet.protect();
protection.removeEditors(protection.getEditors().map(function(editor) {
return editor.getEmail();
var currentSheetName = sheet.getName();
var config = protectionConfig.find(function (_a) {
var sheetName = _a.sheetName;
return sheetName === currentSheetName;
if (config)
function resetSpreadsheetProtections() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var protectionConfig = getProtectionConfig(spreadsheet);
setSpreadsheetProtections(spreadsheet, protectionConfig);
Note: there is also another script needed for this one called
Finally it's working now:
Add the following to the above code:
function addEditorsToSpreadsheetFromProtectionConfig(spreadsheet, protectionConfig) {
var editors = protectionConfig.reduce(function (accumulator, _a) {
var editors = _a.editors;
return accumulator.concat(editors);
}, []);
Then Add to resetSpreadsheetProtections() the following line:
addEditorsToSpreadsheetFromProtectionConfig(spreadsheet, protectionConfig);