Calling a bound script's method using the Execution API - google-apps-script

I'm using PropertiesServices as variables, specifically Document Properties , in order to replace some tokens like "{client name}". Since those properties are scoped to the bound script only, I'm looking for a way to modify their values from my PHP application.
Is it possible to call a bound script's function using the Execution API, or maybe from a standalone script? Otherwise, should I instead use the Script Properties instead (although the docs make me think you can't use them if the script isn't 'standalone).

It looks like if the user that the Execution API is running under has permission to the doc that bound script ran by the execution api can read document properties.
Here is my test:
Create a new spreadsheet. Create a new script. Add some data using the menu from onOpen. Run executeAPI inside the script. The log successfully shows the document properties.
function onOpen() {
var testMenu = SpreadsheetApp.getUi().createMenu("test")
testMenu.addItem("Add some data", "addData").addToUi();
testMenu.addItem("Preview data", "getData").addToUi();
}
function getData(){
var keys = PropertiesService.getDocumentProperties().getKeys();
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().clear().appendRow(keys)
}
function returnData(){
return PropertiesService.getDocumentProperties().getKeys();
}
function addData(){
var DT = new Date().toString()
PropertiesService.getDocumentProperties().setProperty(DT,DT);
}
function executeAPI(){
var url = 'https://script.googleapis.com/v1/scripts/'+ScriptApp.getProjectKey()+':run';
var payload = JSON.stringify({"function": "returnData","parameters":[], "devMode": true});
var params={method:"POST",
headers:{Authorization: "Bearer "+ ScriptApp.getOAuthToken()},
payload:payload,
contentType:"application/json",
muteHttpExceptions:true};
var results = UrlFetchApp.fetch(url, params);
Logger.log(results)
}

Related

How to execute Google App script from external script

I am attempting to build a web app that will create a Google Doc from a template and populate it with user provided data. Using Google's quickstart example in the documentation, I can successfully authorize and access the Google Drive file system. Now I need to programmatically open a template Google Doc (or even create one from scratch) and add the data.
This is rather easily done using App Script's Document Service (the DocumentAppclass). So I can do something like:
function createDoc(contentArray) {
var doc = DocumentApp.create('Sample Document');
var body = doc.getBody();
var rowsData = contentArray; // data submitted with HTML form passed as arg
body.insertParagraph(0, doc.getName())
.setHeading(DocumentApp.ParagraphHeading.HEADING1);
table = body.appendTable(rowsData);
table.getRow(0).editAsText().setBold(true);
}
in a standalone App Script and successfully create the new Google Doc on Google Drive. I can't figure out how to execute this App Script from my external web app. Is there a way to do this or do I need to find a different way to create Google Docs (and add content) using just the Drive API?
EDIT:
here is the GET request from my web app:
var gurl = "https://script.google.com/macros/s/AKfycbwMHKzfZr1X06zP2iEB4E8Vh-U1vGahaLjXZA1tk49tBNf0xk4/exec";
$.get(
gurl,
{ name: "john", time: "2pm",},
function(data) {
console.log(data);
},
"jsonp"
)
and here is my doGet():
function doGet(e) {
var result = "";
var name = e.parameter.name;
Logger.log(name);
try {
result = "Hello " + name;
} catch (f) {
result = "Error: " + f.toString();
}
result = JSON.stringify({
"result": result
});
var doc = DocumentApp.create('ballz3');
var body = doc.getBody();
var rowsData = [['Plants', 'Animals'], ['Ficus', 'Goat'], ['Basil', 'Cat'], ['Moss', 'Frog']];
body.insertParagraph(0, doc.getName())
.setHeading(DocumentApp.ParagraphHeading.HEADING1);
table = body.appendTable(rowsData);
table.getRow(0).editAsText().setBold(true);
Logger.log('DOc Name: ' + doc.getName());
return ContentService
.createTextOutput(e.parameter.callback + "(" + result + ")")
.setMimeType(ContentService.MimeType.JAVASCRIPT);
}
In order to run a script from an external location like your web app you need to follow some set-up and use the Script API. The documentation provided below has a great example on how to run your scripts from outside.
Additionally, you can use the APIs directly, with services like OAuth to use the APIs directly in a way that can save you some time and make your code more simple. Using OAuth can provide you with simple API requests. To use it:
Go to the link provided below, select the desired scope (Drive for this example).
Exchange the authorization token for refresh/access tokens.
Proceed to configure the request. Here you can set all the parameters for the request, and even select from the existing operations the scope has available (“List possible operations” button).
The resulting request will look like the one below:
GET /drive/v3/files HTTP/1.1 Host: www.googleapis.com Content-length:
0 Authorization: Bearer [YOUR-TOKEN]
Beneath it you will see the server response to the request.
Documentation URL: https://developers.google.com/apps-script/api/how-tos/execute
OAuth Playground: https://developers.google.com/oauthplayground/

Google Apps Script create calendar event from sheet forbidden?

In a document-bound Google Appscript in one of our company spreadsheets, I've created a script that turns spreadsheet lines into Google calendar appointments. The function works fine for me, but not for my coworker, even though we both have permissions to edit the calendar and change sharing permissions, and my coworker proved he can create appointments on the calendar from calendar.google.com.
He gets the following error message when he runs the script:
{"message":"Forbidden","name":"GoogleJsonResponseException","fileName":"SCHEDULER","lineNumber":204,"stack":"\tat SCHEDULER:204 (createAppointments)\n"}
Line 204 corresponds to the command:
Calendar.Events.insert(event, CAL, {sendNotifications: true, supportsAttachments:true});
If he has edit rights to the calendar, why is this forbidden? Is there a problem with the Calendar service in Google Apps Script? What is more, I changed the CAL variable to a calendar I personally created and shared out to him with the same permissions. He can edit that calendar just fine.
Here is the psuedocode for the function
function createAppointments() {
var CAL = 'companyname.com_1v033gttnxe2r3eakd8t9sduqg#group.calendar.google.com';
for(/*each row in spreadsheet*/)
{
if(/*needs appointment*/)
{
var object = {/*...STUFF...*/};
var coworker = 'coworker#companyname.com';
var timeArgs = {start: /*UTC Formatted time*/, end: /*UTC Formatted time*/}
if(/*All the data checks out*/{
var summary = 'Name of appointment'
var notes = 'Stuff to put in the body of the calendar appointment';
var location = '123 Happy Trail, Monterrey, TX 12345'
//BUILD GOOGLE CALENDAR OBJECT
var event = {
"summary": summary,
"description": notes,
"start": {
"dateTime": timeArgs.start,
"timeZone": TZ
},
"end": {
"dateTime": timeArgs.end,
"timeZone": TZ
},
"guestsCanInviteOthers": true,
"reminders": {
"useDefault": true
},
"location": location
//,"attendees": []
};
event.attendees = [{coworker#companyname.com, displayName: 'coworker name'}];
//CREATE CALENDAR IN GOOGLE CALENDAR OF CONST CAL
Calendar.Events.insert(event, CAL, {sendNotifications: true, supportsAttachments:true});
} else{/*Tell user to fix data*/}
}
}
Thank you very much!
Update 12/29/2017:
I've Tried adjusting the app according to Jason Allshorn and Crazy Ivan. Thank you for your help, so far! Interestingly, I have run into the same response using both the Advanced Calendar Service and the CalendarApp.
The error is, as shown below:
<!DOCTYPE html><html><head><link rel="shortcut icon" href="//ssl.gstatic.com/docs/script/images/favicon.ico"><title>Error</title><style type="text/css">body {background-color: #fff; margin: 0; padding: 0;}.errorMessage {font-family: Arial,sans-serif; font-size: 12pt; font-weight: bold; line-height: 150%; padding-top: 25px;}</style></head><body style="margin:20px"><div><img alt="Google Apps Script" src="//ssl.gstatic.com/docs/script/images/logo.png"></div><div style="text-align:center;font-family:monospace;margin:50px auto 0;max-width:600px">Object does not allow properties to be added or changed.</div></body></html>
Or, after parsing that through an html editor:
What does that even mean? I have the advanced service enabled, and the script is enabled to run from anyone. Any ideas?
I have confirmed after testing what the error comes back after trying to run the calendarApp/Advanced Calendar event creation command.
Here is my code that caused me to get this far:
function convertURItoObject(url){
url = url.replace(/\+/g,' ')
url = decodeURIComponent(url)
var parts = url.split("&");
var paramsObj = {};
parts.forEach(function(item){
var keyAndValue = item.split("=");
paramsObj[keyAndValue[0]] = keyAndValue[1]
})
return paramsObj; // here's your object
}
function doPost(e) {
var data = e.postData.contents;
data = convertURItoObject(data);
var CAL = data.cal;
var event = JSON.parse(data.event);
var key = data.key;
var start = new Date(event.start.dateTime);
if(ACCEPTEDPROJECTS.indexOf(key) > -1)
{
try{
var calendar = CalendarApp.getCalendarById(CAL);
calendar.createEvent(event.summary, new Date(event.start.dateTime), new Date(event.end.dateTime), {description: event.description, location: event.location, guests: event.guests, sendInvites: true});}
/*try {Calendar.Events.insert(event, CAL, {sendNotifications: true, supportsAttachments:true});} Same error when I use this command*/
catch(fail){return ContentService.createTextOutput(JSON.stringify(fail));}
e.postData.result = 'pass';
return ContentService.createTextOutput(JSON.stringify(e));
}
else {
return ContentService.createTextOutput('Execution not authorized from this source. See CONFIG of target project for details.');
}
}
Your script is using Advanced Google Services, specifically Calendar. Read the section "Enabling advanced services"; everyone will have to follow those steps to use the script.
Alternatively (in my opinion, this is a better solution), rewrite the script so that it uses the standard CalendarApp service. It also allows you to create an event and then you can add various reminders to that event.
A solution from my side would be to abstract the calendar event creation function away from your Spreadsheet bound script to a separate standalone apps-script that runs under your name with your permissions.
Then from your sheet bound script call to the standalone script with a PUT request containing the information needed to update the Calender. This way anyone using your sheet addon can update the calander without any mess with permissions.
The sheet bound script could look something like this:
function updateCalander(){
var data = {
'event': EVENT,
};
var options = {
'method' : 'post',
'contentType': 'application/json',
'payload' : data
};
var secondScriptID = 'STANDALONE_SCRIPT_ID'
var response = UrlFetchApp.fetch("https://script.google.com/macros/s/" + secondScriptID + "/exec", options);
Logger.log(response) // Expected to see sent data sent back
Then your standalone script would look something like this:
function convertURItoObject(url){
url = url.replace(/\+/g,' ')
url = decodeURIComponent(url)
var parts = url.split("&");
var paramsObj = {};
parts.forEach(function(item){
var keyAndValue = item.split("=");
paramsObj[keyAndValue[0]] = keyAndValue[1]
})
return paramsObj; // here's your object
}
function doPost(e) {
var CAL = 'companyname.com_1v033gttnxe2r3eakd8t9sduqg#group.calendar.google.com';
var data = e.postData.contents;
data = convertURItoObject(data)
var event = data.event;
try {
Calendar.Events.insert(event, CAL, {sendNotifications: true, supportsAttachments:true});
}
catch(e){
Logger.log(e)
}
return ContentService.createTextOutput(JSON.stringify(e));
}
Please note, the standalone script needs to be set to anyone can access, and when you make updates to the code be sure to re-publish the code. If you don't re-publish your calls to the standalone script are not made to the latest code.
This is a delayed response, but thanks to all who recommended using the POST method. It turns out the proper way to do this is to use URLFetchApp and pass the Script's project Key to authorize the calendar access (I believe you only need to make sure the person executing the script has rights to edit the actual calendar).
Here is basically how to do it in a functional way:
//GCALENDAR is th e unique ID of the project int it's URL when the script is open for editing
//PROJECTKEY is the unique ID of the project, found in the Project Properties Menu under FILE.
//CREATE CALENDAR IN GOOGLE CALENDAR OF CONST CAL
var data = {
'event': JSON.stringify(event),
'cal': CAL,
'key': PROJECTKEY
};
var options = {
'method' : 'post',
'contentType': 'application/json',
'payload' : data,
'muteHttpExceptions': true
};
var answer = UrlFetchApp.fetch("https://script.google.com/macros/s/" + GCALENDAR + "/exec", options).getContentText();
Logger.log(answer);

Google Apps Script Execution API - Calling app is Apps Script - Apps Script to Apps Script

I have two scripts in different domains, I would like to fetch data from one to the other.
I've found Script Execution API to be useful, however they don't provide any example GAS to GAS. https://developers.google.com/apps-script/guides/rest/quickstart/target-script
Any suggestion/guidance on how to accomplish such thing?
This answer might be helpful. what i have done is that created a cloud platform project and created two standalone project, one contains the calling script and other has the method you want to call and must be deployed as a API Executable.
IMPORTANT: Both these project must be associated with the Cloud Platform Project.
For associating the projects to Cloud Platform's project , in script editor do this, Resources --> Cloud Platform Project. You'll see the dialog box which has input box having placeholder Enter Project Number Here, here put the Cloud Platform's project number and do the same for other project as well.
Now as i have followed google's quick start tutorial for EXECUTION API. i have a target script (Script containing your method) as follows:
/**
* The function in this script will be called by the Apps Script Execution API.
*/
/**
* Return the set of folder names contained in the user's root folder as an
* object (with folder IDs as keys).
* #return {Object} A set of folder names keyed by folder ID.
*/
function getFoldersUnderRoot() {
var root = DriveApp.getRootFolder();
var folders = root.getFolders();
var folderSet = {};
while (folders.hasNext()) {
var folder = folders.next();
folderSet[folder.getId()] = folder.getName();
}
return folderSet;
}
NOTE: This script must be deployed as a API Executable. In project you must have enabled the Google Execution API.
Now the calling script,
function makeRequest(){
// DriveApp.getRootFolder();
var access_token=ScriptApp.getOAuthToken();
var url = "https://script.googleapis.com/v1/scripts/{YourScriptId}:run";
var headers = {
"Authorization": "Bearer "+access_token,
"Content-Type": "application/json"
};
var payload = {
'function': 'getFoldersUnderRoot',
devMode: true
};
var res = UrlFetchApp.fetch(url, {
method: "post",
headers: headers,
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
Logger.log(res);
}
On first line inside function you can see i have commented out the Drive API call. I did that because the calling script and target script must have the same scope.
Yes you can use Execution API to fetch data from one domain to other. From the documentation, it consists of a single scripts resource, which has a single method, run, that makes calls to specific Apps Script functions.
The run method must be given the following information when called:
The ID of the script being called.
The name of the function within the script to execute.
The list of parameters the function requires (if any).
Here is an example from GitHub:
var script = google.script('v1');
var key = require('./creds.json');
var jwtClient = new google.auth.JWT(key.client_email, null, key.private_key, ['https://www.googleapis.com/auth/spreadsheets'], null);
jwtClient.authorize(function(err, tokens) {
if (err) {
console.log(err);
return;
}
script.scripts.run({
auth: jwtClient,
scriptId: 'script-id',
resource: {
function: 'test',
parameters: []
}
}, function(err, response) {
if (err) {
throw err;
}
console.log('response', response);
})
});
You can also check this related SO question on how to use Google Apps Script Execution API and Javascript to make the interaction between website and Google Drive.

Using google.visualization.Query in google web app

I have a functioning google web app that is identical to the app presented [here]
You'll note in code.gs that SpreadsheetApp.openByID......getRange().getValues() is used to retrieve an Array that is later converted into a DataTable in the Dashboard-JavaScript.html
Working Code.gs:
function doGet(e) {
var template = HtmlService.createTemplateFromFile('Index');
// Build and return HTML in IFRAME sandbox mode.
return template.evaluate()
.setTitle('Dashboard demo')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
/**
* Return all data from first spreadsheet as an array. Can be used
* via google.script.run to get data without requiring publication
* of spreadsheet.
* Returns null if spreadsheet does not contain more than one row.
*/
function getSpreadsheetData() {
var sheetId = '-key-';
var data = SpreadsheetApp.openById(sheetId).getSheets()[0].getRange("A1:D8").getValues();
return (data.length > 1) ? data : null;
}
I would like to use google.visualization.query instead of .getRange.
Does not work - currently returns "Google" not defined
function doGet(e) {
var template = HtmlService.createTemplateFromFile('Index');
// Build and return HTML in IFRAME sandbox mode.
return template.evaluate()
.setTitle('Dashboard demo')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
function getSpreadsheetData() {
var opts = {sendMethod: 'auto'};
var sheetId = '-key-';
var query = new google.visualization.Query('http://spreadsheets.google.com?key=' + sheetId, opts);
query.setQuery('select A, B, C, D');
query.send(draw)
}
function draw(response) {
if (response.isError()) {
alert('Error in query');
}
alert('No error')
}
I'm certain there are several issues - but I can't get any helpful errors returned to debug the issue.
My questions are:
Is is possible to use google.visualization.query in code.gs?
(I've read a post that leads me to believe that perhaps it cannot be used server side??/why)
If yes - how do I avoid "google not defined" errors
If no - is there an alternative method to "query" a google sheet from server side (the end goal is to have the flexibility to omit columns, perform aggregate functions, etc. when the datatable is retrieved). It is not possible to change the underlying spreadsheet (ie. sharing and publishing)
Finally- I apologize if any of this is formatted poorly or not clear. This is my first post here. In addition, I have limited experience and less expertise with javascript/apps script and google web apps.
No, it cannot be done server side, because google is a client side API.
Same reason as with google.script.run. (For a better understanding, you can check the whole code yourself, it resides here, a code that needs to be embedded within <script> tags, on the Html side).
As an alternative for the server side, you should be able to use the URLFetchApp.
The URL to compose should look something like:
var BASE_URL = "https://docs.google.com/a/google.com/spreadsheets/d/";
var SS_KEY = "stringSS_Key";
var BASE_QUERY = "/gviz/tq?tq=";
var partialUrl = BASE_URL + SS_KEY + BASE_QUERY;
var queryToRun = 'select dept, sum(salary) group by dept';
var finalUrl = partialUrl + encodeURIComponent(queryToRun);
And call URLFetchApp.fetch on it.

Allow script to run as admin to update spreadsheet

I have a spreadsheet that is shared with some other users. Many of the cell are range protected. However, through a menu I allow a user to run a script (which access an external library therefore invisible and not in the control of the user) that will modify some of the protected ranges. However, the script throws that there is no permission to perform this operation.
Is there any option to have this library run with library 'admin' rights so it doesn't throws due to protection?
Thx!
According to the documentation : "the library does not have its own instance of the feature/resource and instead is using the one created by the script that invoked it. "
So library is not the way to go.
You can achieve that behavior using a standalone script that would run as a service (a doGet() function in a deployed webapp) that you would deploy as "running as you" and that you would call with parameters to tell it what to do on the target spreadsheet range.
Edit : in its most basic implementation you can use a simple script like this one as a server app :
function doGet(e) {
if(e.parameter.mode==null){return ContentService.createTextOutput("error, wrong request").setMimeType(ContentService.MimeType.TEXT)};
var coord = e.parameter.coord;
var mode = e.parameter.mode;
var value = e.parameter.value;
var ss = SpreadsheetApp.openById('11myX1YX_________________FS6BesaBEnQ');
var sh = ss.getSheetByName(e.parameter.sN);
if(mode=='r'){
var sheetValue = JSON.stringify(sh.getRange(coord).getValue());
var valToReturn = ContentService.createTextOutput(sheetValue).setMimeType(ContentService.MimeType.JSON);
return valToReturn;
}
if(mode=='w'){
sh.getRange(coord).setValue(value);
return ContentService.createTextOutput(value).setMimeType(ContentService.MimeType.JSON);
}
return ContentService.createTextOutput('error').setMimeType(ContentService.MimeType.TEXT);
}
The above script should be deployed with the following parameters :
Then you can use it with a simple urlFetch like below :
var url = "https://script.google.com/macros/s/AKfycbxs9M0ib-VRmmcVJ0UUJXmHITOrWcoG8bYrK4EK7Tvl0krzsYc/exec"
function testServerLink(){
var coord = 'A3';//coordinates in A1 notation
var sheetName = 'Sheet1';
var data = 'test value';
var mode = 'w';// w for "write" and r for "read"
var write = sheetService(mode,coord,sheetName,data);
Logger.log(write);//shows the result in logger
var read = sheetService('r','A1',sheetName,data);
Logger.log(read);//shows the value that was in A1 cell
}
function sheetService(mode,coord,sheetName,data){
Logger.log(url+"?mode="+mode+"&coord="+coord+"&sN="+sheetName+"&value="+data);// shows the actual url with parameters, can be tested in a browser
var result = UrlFetchApp.fetch(url+"?mode="+mode+"&coord="+coord+"&sN="+sheetName+"&value="+data);
return result
}