I am sharing a set of Google Sheet documents with other users. I am the owner of all the spreadsheets and they are housed on my Google Drive. Every user has their own dedicated sheet. Users need to be able to run scripts on their sheet that collect and manipulate data from the sheet, which means the sheet needs to be unprotected while the scripts run. However, at the end end of the script, I want to protect the sheet again so that the user running the script is unable to edit the protected cells once the script is done running.
I am able to easily remove the protections from the sheet at the beginning of the script, but I am unable to protect the sheet again via the script without the user who is running the script being listed as an editor. I have tried using the "removeEditors" function to no avail (see below for basic example for one of the protected ranges).
var userEmail = Session.getActiveUser().getEmail();
var protection = newPickSheet.getRange('A1:M3').protect();
protection.removeEditor(userEmail);
For your reference, here is the code I'm using at the beginning of the script to remove all protections:
var protections = newPickSheet.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for(var i = 0; i < protections.length; i++){
protections[i].remove();
}
To summarize, I need my script to:
Remove all protections
Execute the main part of the script
Re-add all protections so that only me, the sheet owner, but NOT the user running the script, has edit ability.
I was able to accomplish this very easily using VBA when deploying Excel Macros, but it seems this is not nearly as simple using App Scripts.
If anyone can help me figure out a solution, it would be greatly appreciated.
You want to run mainly 3 functions in one function.
Remove the protected the range of newPickSheet.getRange('A1:M3').
Run a script for editing cell in the range.
Protect the range.
You want to run the script by clicking run button which is put on Spreadsheet. You don't want to use the event trigger.
If my understanding is correct, I think that the important point is who run each function. So how about this workaround? I think that there are several workarounds for your situation. So please think of this as just one of them.
Modification points:
In your situation, in order to protect a range from users, it is required to run the function for protecting the range as owner. Because the protected range is created by owner, when the protected range is removed, it is required to also run the function as owner.
Here, in this workaround, it supposes that the script of Execute the main part of the script includes the methods depending on user.
When the script is run with clicking the run button by each user, the script is run as each user. This is the important point for this situation. In order to run the part of script as owner, I used Web Apps here. By using Web Apps, the script for removing and adding the protected range can be run as owner.
By above points, the flow of this workaround is as follows.
1. Remove the protected range by owner using Web Apps.
2. Run the script of Execute the main part of the script by each user.
3. Protect the range by owner using Web Apps.
Preparation for using modified script:
Please run the following flow before you use the modified script.
Copy and paste the modified script to the script editor.
This script supposes that you are using the container-bound script of the Spreadsheet.
In this modified script, I used main() for the main function. If you are using other name, please modify it.
Please set ##### of var newPickSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("#####");.
Put the script of "Execute the main part of the script".
Deploy Web Apps.
On the script editor, open "Publish" -> "Deploy as Web Apps".
Set "Project version" as new. Please input freely to "Describe what has changed".
Set "Execute the app as:" to "Me".
Set "Who has access to the app:" to "Anyone". By this, each user can access to Web Apps using own access token.
Click "Deploy" or "Update" button. By this, Web Apps is deployed.
When you modify the script after Web Apps was deployed, please redeploy as new version. By this, the latest script is reflected to Web Apps. This is an important point for using Web Apps.
Modified script:
In this modified script, I used main() for the main function. If you are using other name, please modify it.
// This is the main function. Please set this function to the run button on Spreadsheet.
function main() {
// DriveApp.getFiles(); // This is a dummy method for detecting a scope by the script editor.
var url = ScriptApp.getService().getUrl() + "?access_token=" + ScriptApp.getOAuthToken();
UrlFetchApp.fetch(url + "&key=removeprotect"); // Remove protected range
// do Something: Please put the script of "Execute the main part of the script" here.
SpreadsheetApp.flush(); // This is required to be here.
UrlFetchApp.fetch(url + "&key=addprotect"); // Add protected range
}
function doGet(e) {
var newPickSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("#####"); // Please set here.
if (e.parameter.key == "removeprotect") {
// Remove protected range.
var protections = newPickSheet.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
protections[i].remove();
}
} else {
// Add protected range.
var ownersEmail = Session.getActiveUser().getEmail();
var protection = newPickSheet.getRange('A1:M3').protect();
var editors = protection.getEditors();
for (var i = 0; i < editors.length; i++) {
var email = editors[i].getEmail();
if (email != ownersEmail) protection.removeEditor(email);
}
}
return ContentService.createTextOutput("ok");
}
Note:
In this modified script, the range of "A1:M3" in the sheet of newPickSheet. newPickSheet is declared as var newPickSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("#####"). If you want to modify this, please modify to your situation.
In my environment, when user clicks the run button soon, after user opens the shared Spreadsheet, there was sometimes the case that the function of the button cannot be found. In this case, please wait for completely loading the Spreadsheet.
This is a simple sample script. So please modify this to your situation.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
Related
I have the following situation:
a static site, only html pages
a cookie notice system, with my own cookies, accept and refuse system of cookies setup
Now I need to inject the GA4 script into the head of pages when cookies are accepted, but...
I have already made made that, by appending the script to the head and it is visible on browser, on page reload with inspect elements...and it's working perfect.
When users click on accept cookies, the cookies accept is saved on client's side, and the script is APPENDED to page.
But I need the GA4 script to be somehow INJECTED, to be visible on the source page. Like when I preview the source page in browser to have it there. I don't need it to be injected into the html file itself, but only into the browser.
I did my own research about these days, and now it's killing me, as all I could find was the append way, but that is not injecting it into the source page on browser.
Any advice or guidance would be greatly appreciated.
Note (as I have been asking all the time. I don't want to offend anyone, but that's the best way I can explain where I want to do and what):
the source page I'm talking about is when right click on browser and view source page (there is where I need the GA4 code to be inserted)
and the way I got it to work is when right click > inspect > elements tab - (there i have it now working)
Thank you!
First question would be, why do you want it to be in the actual source code? A common way of inserting these scripts is through a tag-management-solution, which basically follows similar logic as appending scripts to the page (i.e. similar to what you meant by the inspect elements route).
To answer your question;
There is an option to get it into the sourcecode, and that is by checking on the server delivering the HTML whether a user has accepted the cookies, if that is the case deliver the HTML file (or adjust the HTML) to contain the GA4 script, if the user didn't accept: deliver the page without the GA4 script.
Since you mention these are static HTML files, I assume there is no server in place where this kind of logic can be inserted. So the best option is to insert the script afterwards.
Another way would be to insert the tag by default, but disable tracking (haven't tested the below part, also, verify yourself whether in your situation this actually blocks tracking when cookies aren't accepted):
window['ga-disable-GA_MEASUREMENT_ID'] = true;
https://developers.google.com/analytics/devguides/collection/gtagjs/user-opt-out
You could try to add this in your HTML before loading the GA4 tag, similar to something like:
<script>
const gaMeasurementId = 'G-12345678'; //replace with your own MeasurementID
let cookiesDeclined = true; //default to declined cookies
document.cookie.split(';').forEach( (cookie) =>{ //loop through all cookies
const cookie_arr = cookie.split('='); //get key/value pairs for cookies
let name = cookie_arr[0]; //cookiename
let val = cookie_arr[1]; //cookieval
if(name === 'cookieConsent' && val === 'accepted' ){
cookiesDeclined = false; //set the declined status to false when user has accepted the cookies
}
})
window['ga-disable-'+gaMeasurementId] = cookiesDeclined;
//->insert ga4 tag here
</script>
For the past few years, I've been using Sheets as a data source for a web app by using the following code to turn the id into a direct link to a TSV file:
let id="1zD3eIL8LCTJ8F_8U3kWA6k5WPJNKr_UZ_93bnARlMxQ"
let str="https://docs.google.com/spreadsheets/d/"+id+"/export?format=tsv";
var xhr=new XMLHttpRequest();
xhr.open("GET",str);
xhr.onload=function() {/* act on data */ };
xhr.send();
xhr.onreadystatechange=function(e) {
if ((xhr.readyState === 4) && (xhr.status !== 200)) {
/* Show error */
}
It still works on old files, but new ones yield a CORS error:
Access to XMLHttpRequest at 'https://doc-00-0g-sheets.googleusercontent.com/export/l5l039s6ni5uumqbsj9o11lmdc/5filqetsf3ohbeiq2e8vbtf8ik/1593267040000/112894833168181755194/*/1zD3eIL8LCTJ8F_8U3kWA6k5WPJNKr_UZ_93bnARlMxQ?format=tsv' (redirected from 'https://docs.google.com/spreadsheets/d/1zD3eIL8LCTJ8F_8U3kWA6k5WPJNKr_UZ_93bnARlMxQ/export?format=tsv') from origin 'https://viseyes.org' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Works: www.viseyes.org/scale?1LSnAM3A62AQipZfqxDtlOjt4MWJ0fBP22cdyqJqEj5M
Error: www.viseyes.org/scale?1zD3eIL8LCTJ8F_8U3kWA6k5WPJNKr_UZ_93bnARlMxQ
I believe your goal as follows.
You want to retrieve the data with TSV format from the Google Spreadsheet using Javascript.
Your spreadsheet is publicly shared.
For this, how about this answer?
Issue and workaround:
I could confirm the same situation from your question. Unfortunately, I couldn't remove this error. So, in this case, as a workaround, I would like to propose to use Web Apps created by Google Apps Script as the wrapper. By this, the error can be removed. The flow of this workaround is as follows.
Request to Web Apps from Javascript.
At Web Apps, the data is retrieved from "https://docs.google.com/spreadsheets/d/"+id+"/export?format=tsv".
Return the data with the TSV format from Web Apps.
Usage:
Please do the following flow.
1. Create new project of Google Apps Script.
Sample script of Web Apps is a Google Apps Script. So please create a project of Google Apps Script.
If you want to directly create it, please access to https://script.new/. In this case, if you are not logged in Google, the log in screen is opened. So please log in to Google. By this, the script editor of Google Apps Script is opened.
2. Prepare script.
Please copy and paste the following script (Google Apps Script) to the script editor. This script is for the Web Apps.
function doGet() {
let id = "1zD3eIL8LCTJ8F_8U3kWA6k5WPJNKr_UZ_93bnARlMxQ"; // This is from your script.
let str = "https://docs.google.com/spreadsheets/d/"+id+"/export?format=tsv"; // This is from your script.
const value = UrlFetchApp.fetch(str);
return ContentService.createTextOutput(value.getContentText());
}
If your Google Spreadsheet is not publicly shared, please modify as follows.
function doGet() {
let id = "1zD3eIL8LCTJ8F_8U3kWA6k5WPJNKr_UZ_93bnARlMxQ"; // This is from your script.
let str = "https://docs.google.com/spreadsheets/d/"+id+"/export?format=tsv"; // This is from your script.
const value = UrlFetchApp.fetch(str, {headers: {authorization: "Bearer " + ScriptApp.getOAuthToken()}});
return ContentService.createTextOutput(value.getContentText());
// DriveApp.getFiles() // This is used for automatically detecting the scope.
}
3. 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, no access token is required to be request. I think that I recommend this setting for your goal.
Of course, you can also use the access token. At that time, please set this to "Anyone". And please include the scope of https://www.googleapis.com/auth/drive.readonly and https://www.googleapis.com/auth/drive to the access token. These scopes are required to access to Web Apps.
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.
4. Run the function using Web Apps.
When you use this, please modify your Javascript script as follows and test it.
From:
let id="1zD3eIL8LCTJ8F_8U3kWA6k5WPJNKr_UZ_93bnARlMxQ"
let str="https://docs.google.com/spreadsheets/d/"+id+"/export?format=tsv";
To:
let str = "https://script.google.com/macros/s/###/exec";
Note:
When you modified the script of Web Apps, please redeploy the Web Apps as new version. By this, the latest script is reflected to the Web Apps. Please be careful this.
In my environment, I could confirm that when above workaround is used, no error occurs and the data with the TSV format can be retrieved.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
I'm trying to create an add-on for Google Sheets. When the sheet is opened, the script should create a menu item in Add-ons.
The item is added and works when I open the Sheet it's originally bound to. When trying to test it as an add-on with onOpen as a declared function in my script, it always results in the item not being added and an error being logged in the Google Chrome DevTools console for the Sheet opened for the test:
Google Apps Script: We're sorry, a server error occurred. Please wait a bit and
try again.
This error doesn't occur if I comment out the onOpen method.
I've tried running it as an add-on in the following ways with both Auth.none and Auth.limited:
The original copy of the script attached to its original Sheet.
The original copy of the script running with a separate Sheet.
A copy of the script in a separate Google Scripts file running with a different Sheet.
Creating a new Sheet and bounded script with just an empty onOpen function.
These all result in the error above, and when I uncomment the contents of onOpen functions that do have code, none of their code appears to run.
Here's the original copy's code but keep in mind I still get the error even if the contents are commented out and this states that even if createMenu is used for an add-on, it's handled correctly by Google App Script:
function onOpen(e){
var menu = SpreadsheetApp.getUi().createAddonMenu(); // Or DocumentApp or FormApp.
if (e && e.authMode == ScriptApp.AuthMode.NONE) {
// Add a normal menu item (works in all authorization modes).
menu.addItem("Show Sidebar", "showSidebar");
menu.addToUi();
} else {
// Add a new menu (doesn't work in AuthMode.NONE).
var topUI = SpreadsheetApp.getUi();
topUI.createMenu("Mail Merge")
.addItem("Show Sidebar", "showSidebar")
.addToUi();
}
}
The Logger object also doesn't seem to log my messages while running the script as an add-on.
Copying the Sheet over to another account and just having it be owned by that account (not in a team drive) did allow the menu to appear when testing it as an add-on with both types of authorization. The Google App Script error also stopped occurring after doing this.
The most closely related information I could find about Team Drive restrictions was this, which says it limits cloud platform interaction. But this link is Google's page on setting up and running Test As Add-on and it doesn't state anything about the Team Drive or cloud platform.
If I have JS script on several different websites, is it possible to enable or disable script execution for specific sites? If this isn't possible, other suggestions for implementation are welcome.
Here's the application:
I have a script tag with my JS source link that site owners can put on their website to enable interaction with my service. However, I would like to be able to enable/disable the service for specific sites so as not to deploy it until they are ready.
note: The script tag also includes site verification information so it gets put on their site before they are ready to deploy. This saves the step of putting in a site verification tag and then going back and putting in the script.
You would want them to put your .ashx handler on your website, then make that return the javascript.
Follow tutorial for ashx page if you are unsure how to http://www.brainbell.com/tutorials/ASP/Generic_Handlers_(ASHX_Files).html
in the processrequest() function:
Check to see if they have it enabled you can use the querystring to see which website it is https://msdn.microsoft.com/en-us/library/system.web.httprequest.querystring(v=vs.110).aspx in the code below there is ?yourwebsitedomain=customersdomain so you would query for "yourwebsitedomain" and you would get "customersdomain"
If they do then Get the bytes of your file using Encoding.UTF8.GetBytes(File.ReadAllText(filename))
and write the results to the output
context.Response.OutputStream.Write(FileBytes, 0, FileBytes.Length);
context.Response.OutputStream.Flush();
Your Customers Website:
<script>
(function() {
var c = document.createElement('script');
c.type = 'text/javascript'; c.async = true;
c.src = "http://yourdomane/yourhandler.ashx?yourwebsitedomain=customersdomain";
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(c,s);
})();
</script>
Hope this helps.
I have an old html page that creates a script file and executes it using:
fsoObject = new ActiveXObject("Scripting.FileSystemObject")
wshObject = new ActiveXObject("WScript.Shell")
I am trying to modify it and make it usable also from other browsers. If you know the answer stop reading and please answer. If there is no quick answer, here is the description of my attempts. I was successful in doing the job, but only when the script is shorter than 2000 characters. I need help for scripts longer than 2000 characters.
The webpage is for internal use only, so it is easy for me to create a custom URL protocol on each computer that runs a VBScript file from a network drive.
I created my custom URL Protocol that starts a VBScript file like this:
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\MyUrlProtocol]
"URL Protocol"=""
#="Url:MyUrlProtocol"
"UseOriginalUrlEncoding"=dword:00000001
[HKEY_CLASSES_ROOT\MyUrlProtocol\DefaultIcon]
#="C:\\Windows\\System32\\WScript.exe"
[HKEY_CLASSES_ROOT\MyUrlProtocol\shell]
[HKEY_CLASSES_ROOT\MyUrlProtocol\shell\open]
[HKEY_CLASSES_ROOT\MyUrlProtocol\shell\open\command]
#="C:\\Windows\\System32\\WScript.exe \"X:\\MyUrlProtocol.vbs\" \"%1\""
In MyUrlProtocol.vbs I have this:
MsgBox "The length of the link is " & Len(WScript.Arguments(0)) & " characters"
MsgBox "The content of the link is: " & WScript.Arguments(0)
When I click on click me I see two messages, so everything works well (tested with Chrome and IE in Windows 7.)
It works also when I execute document.getElementById("test").click()
I thought this could be the solution: I would pass the text of the script to the VBS static script, which would create the dynamic script and run it, but with this system I can't pass more than ~2000 characters.
So I tried to split the text of the script in chunks smaller than 2000 characters and simulate several clicks on the link, but only the first one works.
So I tried with xmlhttp.open("GET","MyUrlProtocol:test",false);, but Chrome says Cross origin requests are only supported for HTTP.
Is it possible to pass more than 2000 characters to a VBScript script via a custom URL protocol?
If not, is it possible to call several custom URL protocols in sequence?
If not, is there another way to create a script file and run it from Javascript?
EDIT 1
I found a solution, but in Chrome only works when it likes, so I'm back to square one.
The code below in IE executes the script 4 times (correct), but in Chrome only the first execution runs.
If I change it to delay += 2000, then Chrome usually runs the script 2 times, but sometimes 1 and sometimes 3 or even 4 times.
If I change it to delay += 10000, then it usually runs the script 4 times, but sometimes misses one.
The function is always executed 4 times, both in Chrome and IE. What is weird is that the sr.click() sometimes does nothing and the function execution continues.
<HTML>
<HEAD>
<script>
var delay;
function runScript(text) {
setTimeout(function(){runScript2(text)}, delay);
delay += 100;
}
function runScript2(text) {
var sr = document.getElementById('scriptRunner');
sr.href='intelliclad:'+text;
sr.click();
}
function test(){
delay = 0;
runScript("uno");
runScript("due");
runScript("tre");
runScript("quattro");
}
</script>
</HEAD>
<BODY>
<input type="button" value="Run test" onclick="test()">
scriptRunner
</BODY>
</HMTL>
EDIT 2
I tried with Luke's suggestion of setting the next timeout from inside the call back but nothing changed (IE works always, Chrome whenever it likes).
Here is the new code:
var scripts;
var delay = 2000;
function runScript() {
var sr = document.getElementById('scriptRunner');
sr.href = 'intelliclad:' + scripts.shift();
sr.click();
if(scripts.length)
setTimeout(function() {runScript()}, delay);
}
function test(){
scripts = ["uno", "due", "tre", "quattro"];
runScript();
}
Some background: The page asks for the shape of a panel, which can be just a few parameters [nfaces=1, shape1='square', width1=100] or hundreds of parameters for panels with many faces, many slots, many fasteners, etc. After asking for all the parameters a script for our internal 3D CAD (which can be larger than 20KB) is generated and the CAD is started and asked to execute the script.
I would like to do all on the client side, because the page is served by a Domino web server, which can't even dream of managing such a complex script.
I didn't read your whole post...have an answer:
I too wish that custom url protocols can handle long urls. They simply do not. IE is even worse as some OSs only accept 800 chars.
So, here's the solution:
For long urls, only pass a single use token. The vbscript uses the token
and does a url get to your web server to get all of the data.
This is the only way I've been able to successfully pass lots of data around. If you ever find a clearer solution, please remember to post it here.
Update:
Note that this is the best way I have found to deal with the url protocol limitations. I too wish this was not necessary. This does work and works well.
You mentioned Dominos, so possibly you need something in a POS environment... I create a web based POS system, so we could face a lot of the same issues.
Suppose you want a custom url to print a pdf to the default printer without the annoying popup window. We need to do this thousands of times a day...
When building the web page, add the print button which when pressed calls the custom url: myproto://printpdf?id=12345&tocken=onetimetoken
this will execute your vbscript on the local desktop
in your vbscript, parse the arguments and react. In this case, your command is printpdf and the id is 123456 and you have a onetime tocken key.
have the vb script to an https get to: https://mydomain.com/APIs/printpdf.whatever?id=12345&key=onetimetoken
check the credentials based on the ip address and token, if all aligns, then return the contents of the pdf (you may want to convert the pdf to a byte array string)
now the vbscript has the pdf, assemble it and write it to a temp folder then execute a silent pdf print command (I use Sumatra PDF http://blog.kowalczyk.info/software/sumatrapdf/free-pdf-reader.html)
mission accomplished.
Since I do know what you what to do in your custom url and the general workflow, I can only describe how I've solved the sort url issue.
Using this technique, the possibilities are limitless. You have full control over the local computer running the web browser, you have a onetime use token which grants access to a web API with can return any sort of information you program.
You could write a custom url protocol to turn on the pizza oven if you wanted :)
If you are not able to create the server side code which is listening for vbscript's get request then this would not work.
You might be able to pass the data from the browser to the vbscript using the clipboard.
Update 2:
Since in this case the data is on the client (one single form can define hundreds of parameters), the server API doesn't know what to answer to the vb script request. So the workflow described above must be preceded by these two steps:
The onkeypress event executes a submit to send the current parameters to the server
The server replies with the refreshed form, adding to the body onload a call to a function which uses another submit to call the custom url, as described on point 1 listed above.
Update 3:
stenci, what you've added (in Update 2) will work. I would do it like this:
user presses a button saying I'm done editing the form
ajax post the form to the server
the server saves the data and attaches unique key to the datastore
the server returns the key to ajax callback function
now the client has a single use key and invokes the url schema passing the key
vbscript does an https get to the server and passes the key
server returns the data to the vbscript
It is a bit long winded. Once coded it will work like a charm.
The only other alternative I can see is to copy the form data to the clipboard using something like: http://zeroclipboard.org/
and then in vbscript see if you can read the clipboard like: Use clipboard from VBScript
How about creating an iFrame for each instance?
Something like this:
function runScript(text) {
var iframe = document.createElement('iframe');
iframe.src = 'intelliclad:'+text;
document.body.appendChild(iframe);
}
function test(){
runScript("uno");
runScript("due");
runScript("tre");
runScript("quattro");
}
You can then use css styling to make these iframes transparent / hidden.
You might not like this answer, but I've used this method in the past and it works.
Instead of relying on ActiveX, consider using a Java Applet, and JNI.
Basically, you have to make sure the native scripts you want to run are available on your client machine, along with a JNI wrapper.
The applet will have to be at least self signed, for the browser to allow it to load and access a native library. Once the JNI libraries are loaded, you can easily call methods from the page / applet.
As a consequence of using Java, you could possibly use the same applet for windows as well as linux clients, provided of course you have native libraries present on the respective clients.
This series of articles talks about precisely your problem : http://www.javaworld.com/article/2076775/java-security/escape-the-sandbox--access-native-methods-from-an-applet.html
P.S the article is really old, but the concept remains unchanged.