Google Apps Script Add On google.run.script runs as developer? - google-apps-script

I've only been working in GAS for a little under 6 months. Everything thus far has been bound spreadsheet projects, but have created quite a few. I have a new need to create an add-on sidebar that people can use in any spreadsheet they want (in a domain). I created the code, published it so only the domain can see it, and can install it as another user in the same domain, all of that seems to work fine.
The issue has to do with the sidebar.html, and calling a function in my script using google.script.run to call a function that gets some external data and writes it to the current sheet. All the code to get the external data works just fine, it all goes into a 2 dimensional array. The part where it breaks is at SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(1,1,x,y).setValues(array). I get a "You do not have permissions to access the requested document".
I did some of the following during testing:
1) The script for the add-on started out as a bound script to an existing sheet for development. Thinking that might be the issue, I re-created the project as a stand alone GAS and published it as a web add-on for sheets. That didn't make a difference.
2) The developer can run the add-on just fine, no sidebar error.
3) I created another menu item for the add-on that, rather than opening a sidebar and letting the sidebar call the function via google.script.run, runs the function directly from the menu. Doing it that way, it 'works' for the other domain user (where it fails running it from the sidebar).
4) I 'shared' the underlying Google Sheet I was using to test the add-on with the domain test user with the developer, and the sidebar script will then work. If I use the add-on in another non-shared sheet, I get the permission error.
5) If I use it on a sheet in a big, shared Drive folder we all use and have access to (domain wide), it works fine, just as I would expect from the test in '3' above.
The Big Questions:
So it seems as though 'google.script.run' runs as the developer, and
not as the current user (at the keyboard)? Is that right? Looking in
documents like this, I couldn't find anything that would indicate
that. Is there something I should be adding to make sure this works?
Is it a side effect of the way I'm publishing it, for domain use only?
Update: Without identifying one in particular, I tested out someone's 'Find and Replace' add-on, and sure enough, it's able to write out from the sidebar directly to the sheet without any issues. The only thing I did was authorize the app when I installed it. So it's clearly possible. I'm just trying to figure out what I might be missing from my add-on that will allow me to do the same.
Here is a small, sample I put together just to test the 'write' of an array of 2 columns and 2 rows out to A1:B2 in the currently active sheet. It works from both the menu and the sidebar as the developer. It always works from the menu as a domain user, but will 'only' work from the sidebar if the developer has direct permissions to the file (either via a one-off share or via being created in a previously shared folder the developer has the appropriate permissions to).
// scopes
https://www.googleapis.com/auth/script.container.ui
https://www.googleapis.com/auth/spreadsheets
// code.gs
function onOpen(e) {
SpreadsheetApp.getUi().createAddonMenu()
.addItem('Sidebar Test', 'showSidebar')
.addItem('setValues Test', 'setValuesTest')
.addToUi();
}
function showSidebar() {
var htmlTemplate = HtmlService.createTemplateFromFile('sidebar');
var ui = htmlTemplate.evaluate()
.setTitle('Sidebar Test')
SpreadsheetApp.getUi().showSidebar(ui);
}
function onInstall(e) {
onOpen(e);
}
function setValuesTest() {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var valArr = [];
valArr.push([1,2]);
valArr.push([3,4]);
sheet.getRange(1, 1, 2, 2).setValues(valArr);
} catch (e) {
throw new Error(errorMessage({e: e}));
}
}
function errorMessage(params) {
return params.e.message + '<br/><div class="main">file: '+
params.e.fileName+'<br/>line: '+ params.e.lineNumber + '</div>';
}
function getVersion() {
var scriptProps = PropertiesService.getScriptProperties();
return scriptProps.getProperty('Version');
}
// utility.gs
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename)
.getContent();
}
// sidebar.js.html
<script>
$(function() {
$('#run-query').click(runQueryButton);
});
function runQueryButton() {
runQuery(this);
}
function runQuery(element) {
element = (element === 'undefined') ? this : element;
element.disabled = true;
$('#error').remove();
google.script.run
.withSuccessHandler(
function(msg, element) {
element.disabled = false;
})
.withFailureHandler(
function(msg, element) {
showError(msg, $('#main'));
element.disabled = false;
})
.withUserObject(element)
.setValuesTest();
google.script.host.editor.focus();
}
function showError(msg, element) {
var div = $('<div id="error" class="error">' + msg + '</div>');
$(element).after(div);
}
</script>
// sidebar.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
<?!= include('sidebar.css'); ?>
</head>
<body>
<div class="sidebar branding-below">
<div id="main" >
<button class="blue cursor-pointer" id="run-query" data-toggle="modal">Execute</button>
</div>
</div>
<div class="sidebar bottom">
Version: <?!= getVersion(); ?>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<?!= include('sidebar.js'); ?>
</body>
</html>
// sidebar.css.html
<style>
.error {
padding: .25em .25em;
font-family: Arial,sans-serif;
}
</style>
Update 2
I changed the deployment type to 'Only Trusted Testers', and added my non-Domain self as that tester. I sent my(other)self the link to the app and installed it from there. Answered the authorization pop-up (though, since I'm not in the domain, I had to go the 'unsafe' route, which is fine). Ran the test add-on...and it worked like a champ! Now, both myself (non-domain) and other, developer self (domain) have paid $5. Is that the issue? Is the issue that the test user I created 'in' the domain hasn't paid any money, so it won't work?
That doesn't make much sense to me, as the test add-on installs, and the test user (in the domain) can call the underlying script from a 'menu', and it populates the spreadsheet just fine. It only breaks down for the domain user when they try to run the app from the side bar, which calls the code with a google.script.run call. This is really, really weird, and frustrating.
On another side note, I added a function to show the 'effective user' on the side bar, just in case there was something hinky happening on that front, and that seems all well and good, reporting the name of the test user in the domain and not the name of the developer in the domain.
I guess my next testing will be to:
Flip it back to domain, reinstall for the domain test user, see if something else didn't happen to fix it.
If still broken, flip it back to trusted testers, add the domain test user to that group, pay another $5, and see if they can install and run it in that fashion. If they can, then there is something either flawed with domain publishing of add-ons, or I'm missing some documentation (or something undocumented).

google.script.run will always run as the effective user of the add-on (which may or may not be the developer).
However if the user is not granted permission to edit the spreadsheet you're going to have errors.
You can try setting up a team-drive for your domain where documents can be easily shared with users in the team. Alternatively you can create a shared folder (with edit rights) for all the users in your domain to store documents that should be accessible by all users in said domain.

Related

How to determine why an alert dialog was displayed by Chrome?

I am trying to fix a bug in a Chrome extension. When the extension is installed an alert dialog containing the message "undefined" will be displayed seemingly at random. This does not happen when the extension is not installed.
There is not one call to alert, confirm, or prompt in the extension source code. How do I find out why the alert dialog is being displayed?
I have attempted adding the following code to one of the background scripts and to one of the content scripts.
var originalWindowAlert = window.alert;
window.alert = function() {
console.trace();
return originalWindowAlert.apply(window, arguments);
}
I have confirmed that this technique works when used in a webpage, but it is not working for the extension.
I have also built Chromium from source code and I am able to reproduce it but so far I have not been able to figure out how to determine the origin of the alert dialog. I have set a breakpoint in the RenderFrameHostImpl::RunModalAlertDialog function but I see no way to determine what caused the breakpoint to be hit.
I am getting desperate.
I asked this question on the Chromium Extensions Google Group. I got the following very useful response from Scott Fortmann-Roe.
If you do the following in a content script:
var originalWindowAlert = window.alert;
window.alert = function() {
console.trace();
return originalWindowAlert.apply(window, arguments);
}
I don't believe it will actually intercept alerts triggered by the page as you are overriding the content script's window.alert method which is different from the page's method (content script JS is isolated from page JS).
To modify the page's alert method you'll probably need to inject a script tag into the page. E.g. something along these lines in the content script:
let script = document.createElement('script');
script.textContent = `
var originalWindowAlert = window.alert;
window.alert = function() {
console.trace()
return originalWindowAlert.apply(window, arguments);
} `;
document.body.appendChild(script);

Programmatic injection on nested iframes in extension page

Summary: I need to find a way to accomplish with programmatic injection the same exact behaviour as using content_scripts > matches with "all_frames": true on a manifest. Why? because it is the only way I've found of injecting iframe's content in an extension page without having Cross-Origin errors.
I'm moving to optional_permissions on a Chrome extension and I'm on a dead end.
What I want:
Move this behaviour to optional_permissions in order to be able to add more hosts in the future. With the current code, by adding one new host on content_scripts > matches the extension is disabled by Chrome.
For the move, I removed content_scripts in the manifest and I added "optional_permissions": ["*://*/"],. Then, I successfully implemented a dialog asking new permissions to the user with chrome.permissions.request.
As I said before, the problem is how to inject the iframe's content in an extension page.
What I've tried:
chrome.declarativeContent.RequestContentScript (mentioned here) with allFrames: true. I can only see the script running if I enter the URL directly, nothing happens when that URL is set in an iframe.
chrome.tabs.onUpdated: url is undefined for an extension page. Also, the iframe url is not detected.
Call chrome.tabs.executeScript with allFrames: true as soon as I load the first iframe. By doing this I get an exception Cannot access contents of the page. Extension manifest must request permission to access the respective host. and the "respective host" is chrome-extension://, which is not a valid host if you want to add it to the permissions.
I'm lost. I couldn't find a way to simulate the same behaviour as content_scripts > matches with programmatic injection.
Note: using webNavigation API is not an option since the extension is live and it has thousands of users. Because of this, I can not use the frameId property for executeScript. Thus, my only option with executeScript was to inject all frames but the chrome-extension host issue do not let me continue.
Update: I was able to accomplish what I wanted but only on an HTTP host. I used chrome.tabs.executeScript (option 3).
The question remains on how to make this work on an extension page.
You cannot run content scripts in any extension page, including your own.
If you want to run code in a subframe of your extension page, then you have to use frameId. There are two ways to do this, with and without webNavigation.
I've put all code snippets in this answer together (with some buttons to invoke the individual code snippets) and shared it at https://robwu.nl/s/optional_permissions-script-subframe.zip
To try it out, download and extract the zip file, load the extension at chrome://extensions and click on the extension button to open the test page.
Request optional permissions
Since the goal is to programmatically run scripts with optional permissions, you need to request the permission. My example will use example.com.
If you want to use the webNavigation API too, include its permission in the permission request too.
chrome.permissions.request({
// permissions: ['webNavigation'], // uncomment if you want this.
origins: ['*://*.example.com/*'],
}, function(granted) {
alert('Permission was ' + (granted ? '' : 'not ') + 'granted!');
});
Inject script in subframe
Once you have a tab ID and frameId, injecting scripts in a specific frame is easy. Because of the tabId requirement, this method can only work for frames in tabs, not for frames in your browserAction/pageAction popup or background page!
To demonstrate that code execution succeeds, my examples below will call the next injectInFrame function once the tabId and frameId is known.
function injectInFrame(tabId, frameId) {
chrome.tabs.executeScript(tabId, {
frameId,
code: 'document.body.textContent = "The document content replaced with content at " + new Date().toLocaleString();',
});
}
If you want to run code not just in the specific frame, but all sub frames of that frame, just add allFrames: true to the chrome.tabs.executeScript call.
Option 1: Use webNavigation to find frameId
Use chrome.tabs.getCurrent to find the ID of the tab where the script runs (or chrome.tabs.query with {active:true,currentWindow:true} if you want to know the current tabId from another script (e.g. background script).
After that, use chrome.webNavigation.getAllFrames to query all frames in the tab. The primary way of identifying a frame is by the URL of the page, so you have a problem if the framed page redirects elsewhere, or if there are multiple frames with the same URL. Here is an example:
// Assuming that you already have a frame in your document,
// i.e. <iframe src="https://example.com"></iframe>
chrome.tabs.getCurrent(function(tab) {
chrome.webNavigation.getAllFrames({
tabId: tab.id,
}, function(frames) {
for (var frame of frames) {
if (frame.url === 'https://example.com/') {
injectInFrame(tab.id, frame.frameId);
break;
}
}
});
});
Option 2: Use helper page in the frame to find frameId
The option with webNavigation looks simple but has two main disadvantages:
It requires the webNavigation permission (causing the "Read your browsing history" permission warning)
The identification of the frame can fail if there are multiple frames with the same URL.
An alternative is to first open an extension page that sends an extension message, and find the frameId (and tab ID) in the metadata that is made available in the second parameter of the chrome.runtime.onMessage listener. This code is more complicated than the other option, but it is more reliable and does not require any additional permissions.
framehelper.html
<script src="framehelper.js"></script>
framehelper.js
var parentOrigin = location.ancestorOrigins[location.ancestorOrigins.length - 1];
if (parentOrigin === location.origin) {
// Only send a message if the frame was opened by ourselves.
chrome.runtime.sendMessage(location.hash.slice(1));
}
Code to be run in your extension page:
chrome.runtime.onMessage.addListener(frameMessageListener);
var randomMessage = 'Random message: ' + Math.random();
var f = document.createElement('iframe');
f.src = chrome.runtime.getURL('framehelper.html') + '#' + randomMessage;
document.body.appendChild(f);
function frameMessageListener(msg, sender) {
if (msg !== randomMessage) return;
var tabId = sender.tab.id;
var frameId = sender.frameId;
chrome.runtime.onMessage.removeListener(frameMessageListener);
// Note: This will cause the script to be run on the first load.
// If the frame redirects elsewhere, then the injection can seemingly fail.
f.addEventListener('load', function onload() {
f.removeEventListener('load', onload);
injectInFrame(tabId, frameId);
});
f.src = 'https://example.com';
}

Button doesn't work on google spreadsheet

I am working to link an image in my Google Sheet document to a specific cell in another tab. I'm doing this by building a simple function that will do this. However, when I assign the function and then click on the image, I then get the error :
Script function "test" could not be found
When I run the function in the script manager interface, it works fine. It's when I try to actually use it in the sheet with the image.
Function Script :
function test()
{
Browser.msgBox("You clicked it!");
}
It turned out that the document owner had left their job and ownership rights had been moved to someone else. Can it matter ?
The error is : Here
Thank you very much,
Make sure the assigned function name does not include the () brackets. 😎
Did a little snippet just to demonstrate on how you might approach this:
I used the sample from HTML Service: Create and Serve HTML on creating a button (in your case it's image) which responds to a click event. I'm using a bound script.
//in bounds script, this integral function triggers as soon as you open the spreadsheet
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Dialog')
.addItem('Click Me', 'test')
.addToUi();
}
//Then I attached your test function
function test()
{
Browser.msgBox("You clicked it!");
}
And sure enough upon clicking the button, the test function triggered:

Posting a status message to Facebook?

There's so many questions regarding Facebook's sharer.php, but they're all out of date. At first Facebook depreciated it, but according to FB's dev documentation it's now back. (Edit: And now it's going again...?)
You used to be able to use it like this:
http://www.facebook.com/sharer/sharer.php?u=<url to share>&t=<message text>
But the documentation now only mentions this:
https://www.facebook.com/sharer/sharer.php?u=<url to share>
Is it possible to set some pre-entered text into the dialogue box that appears when sharing a link on Facebook?
Thanks.
The Share dialog takes only the URL to share as parameter, nothing else (title, description, picture, …) any more. It fetches this data from the URL itself, from the Open Graph meta elements embedded into the document, or it takes a “guess” from the content if those are not present.
And even the “older” version of the Share dialog has not been taking a pre-set message parameter for a long time now – because you are not supposed to pre-fill the message in any way when sharing something, not matter what way the share actually happens. (“Not supposed to” actually meaning, Platform Policies explicitly forbid you from doing so.)
You can of course also share links via API (rather called “posting” a link then) – and because that happens in the background, the message is a parameter you specify while doing so. But the same rules apply – the message is supposed to be created by the user themselves beforehand, which effectively means they should have typed it in somewhere before. And even there it should not have been pre-filled so that they just have to press enter or click a button.
And since they announced API v2.0, all new apps will have to go through “login review” before they will be able to ask for any advanced permission (and posting a link requires one) – and with a pre-filled message in your app’s posting flow, you will definitively not get approval. Of course, you could try to “cheat” on that, and implement the pre-filling of the message only afterwards … but again, doing so is a clear violation of Platform Policies, and will get your app blocked when you are caught doing so.
And if you are planning to do this for multiple users with the same or largely similar messages, you can assume that Facebook’s algorithms will catch that quite easily.
Just one small comment - while it is not possible to edit the text as the other comments say - it is possible to edit everything going on in that page if you can install a browser extension on your client's machines (you did not specify your use case so I am mentioning this just in case you are developing something that you are able to influence in the client machine level).
For example, with a chrome extension, you can inject scripts into facebook.com domain. in the extension manifest.json:
"content_scripts": [
{
"matches": ["https://*.facebook.com/*",
And then this might be your contnet script, where you can play around with the text by hooking up to the markeup. This example sends out analytics (facebook sharer conversion rate) and changes some text (from "share" to "upload" to facebook):
sharer = (function () {
var _ref = qs('ref') ? qs('ref') : 'unknown';
function qs(name) {
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
function isSharer() {
return location.pathname === '/sharer/sharer.php';
}
function bindEvents() {
$('button[name="share"]').click(function() {
analytics.send('fb_sharer', 'share', _ref);
});
$('#u_0_0').click(function() {
analytics.send('fb_sharer', 'cancel', _ref);
});
}
function changeText() {
console.log($('.fcw').length);
$('.fcw').text('Upload to Facebook');
}
function load() {
if (!isSharer()) return;
changeText();
analytics.send('fb_sharer', 'view', _ref);
bindEvents();
}
return {
load: load
}
})();

Is there a way to remove a script from a doc (using the new doc embedded script)

I developed a script extension that uses a Google doc as template AND as script holder.
It gives me a very nice environment to implement a mail merge application (see below).
At some point I use the DocsList class makeCopy(new Name) to generate all the docs that will be modified and sent. It goes simply like that :
var docId=docById.makeCopy('doc_'+Utilities.formatString("%03d",d)).getId();
Everything works quite nicely but (of course) each copy of the template doc contains a copy of the script which is obviously not necessary ! It is also a bit annoying since each time I open a copy to check if data are right I get the sidebar menu that opens automatically which is a time consuming process ...
My question is (are) :
is there any way to remove the embedded script from the copy ? (that would be simple)
or should I copy all the doc elements from the template to an empty document ? (which is also a possible way to go but I didn't try and I don't know what will be in this doc in real life use...
Shall I get a perfect clone in any case ?)
I've read the doc and didn't find any relevant clue but who knows ? maybe I missed something obvious ;-)
below is a reduced screen capture to show the context of this question :
Following Henrique's suggestion I used a workaround that prevents the UI to load on newly created documents... (thanks Henrique, that was smart ;-)
The function that is called by onOpen now goes like that :
function showFields() {
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
var find = body.findText('#'); // the new docs have no field markers anymore.
if(find != null){ // show the UI only if markers are present in the document.
var html = HtmlService.createHtmlOutputFromFile('index')
.setTitle("Outils de l'option Publipostage").setWidth(370);
ui.showSidebar(html);
}
}