Google script embedded in google site does not work - google-apps-script

I have a google script published as a web app which uses browser location service to check the users location. The script works without any issues and shows exactly what is expected of it when deployed and tested.
However if the said URL is embedded into new google sites it stops working. I believe it has something to do with the iframe.
This is the script and the index.html contents. Please note that it was adapted from a different thread here, it is not my script.
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<button id = "find-me">Show my location</button><br/>
<p id = "status"></p>
<a id = "map-link" target="_blank"></a>
<script>
function geoFindMe() {
const status = document.querySelector('#status');
const mapLink = document.querySelector('#map-link');
mapLink.href = '';
mapLink.textContent = '';
function success(position) {
const latitude = position.coords.latitude;
const longitude = position.coords.longitude;
google.script.run.saveCoordinates(latitude,longitude);
status.textContent = '';
mapLink.href = 'https://www.openstreetmap.org/#map=18/' + `${latitude}\/${longitude}`;
mapLink.textContent = `Latitude: ${latitude} °, Longitude: ${longitude} °`;
}
function error() {
status.textContent = 'Unable to retrieve your location';
}
if(!navigator.geolocation) {
status.textContent = 'Geolocation is not supported by your browser';
} else {
status.textContent = 'Locating…';
navigator.geolocation.getCurrentPosition(success, error);
}
}
document.querySelector('#find-me').addEventListener('click', geoFindMe);
</script>
</body>
</html>
Code:
function doGet(e) {
var html = HtmlService.createHtmlOutputFromFile("index");
return html.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
When run through the Google site it returns: Unable to retrieve your location
I have tried all of the permission combinations when deploying and nothing works. Most likely something to do with the way google handles web apps and their embedding but I can't seem to figure out why it doesn't work.

Related

Can I modify HtmlService Output using Google Apps Script?

I made an web app which copies folder and files of google drive. My goal is to show the name of the file and folder which is being copied on the web page('messages' id). Is it possible to change the web page from the google apps script?
When I open the web app, the GAS shows an output page using a form.html, and when I push the copy button, it will start start() function and begin copying the folders. After the successful process, the successHandler will call onSuccess() and it will change the innerHTML to Success.
I'd like to change the innerHTML during the copy process according to the folders' and files' name, but I don't know how to change it from the GAS function start() or copyFolder().
Thank you.
code.gs
function doGet(){
return HtmlService.createHtmlOutputFromFile('form');
}
function start() {
var sourceFolderId = "FOLDERID";
var targetFolder = "TARGET_FOLDER_NAME";
var source = DriveApp.getFolderById(sourceFolderId);
var target = DriveApp.createFolder(targetFolder);
copyFolder(source, target);
}
function copyFolder(source, target) {
var folders = source.getFolders();
var files = source.getFiles();
while(files.hasNext()) {
var file = files.next();
file.makeCopy(file.getName(), target);
}
while(folders.hasNext()) {
var subFolder = folders.next();
var folderName = subFolder.getName();
var targetFolder = target.createFolder(folderName);
copyFolder(subFolder, targetFolder);
}
}
form.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
function callFolderDownload() {
document.getElementById('messages').innerHTML = 'Copying...';
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(onFailure)
.start();
}
function onSuccess() {
document.getElementById('messages').innerHTML = 'Success';
}
function onFailure(error)
{
document.getElementById('messages').innerHTML = error.message;
}
</script>
</head>
<body>
<div id="messages">Click the button to copy the folder.
</div>
<div>
<button type="button" onclick='callFolderDownload();' id="download">Copy</button>
</div>
</body>
</html>
You would need to break up your execution into various parts
You can't "stream" updates from the server process of copying a folder.
At the expense of making your execution slower, you can break it up into various phases, and at the end of each phase, update the client side with status. Remember that for every different stage you are having to:
- Make a call to Apps Script from client.
- Apps Script makes a separate call to Drive.
- Drive responds to Apps Script.
- Apps Script responds to client.
To get a message for each file copied, this needs to happen for each file. Depending on how many files you need to copy, this may not work. In that case, you would probably have to do it in batches. That is, maybe when you get the list of files back from the server, you can send 10 to the server to copy per request.
The key is that there has to be a separate call and response from server for each "real time update". These calls and responses need to be asynchronously programmed, that is, each stage needs to wait for previous stages to complete. This can result in the "pyramid of doom" which can be quite ugly and hard to understand:
google.script.run
.withSuccessHandler((targetId) => {
google.script.run
.withSuccessHandler((list) => {
newMessage("Starting to copy...");
list.forEach((item) => {
google.script.run
.withSuccessHandler(() => {
newMessage(item.name + " has been copied");
})
.copyItem(item.id, targetId);
});
})
.getFolderItems();
})
.createTargetFolder(targetName);
This can be made better with promises or the async await syntax, but in the interest of not making this answer too long, I have omitted it.
Below is an example of it working:
HTML
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
// This is a utility function to append a new message to the html
function newMessage(message) {
let messageElement = document.getElementById("messages")
let sourceDiv = document.createElement("div")
sourceDiv.innerText = message
messageElement.append(sourceDiv)
}
function copyFolder() {
newMessage("Preparing to copy...")
let targetName = document.getElementById("newFolderName").value
console.log(targetName)
google.script.run
.withSuccessHandler(targetId => {
google.script.run
.withSuccessHandler(list => {
newMessage("Starting to copy...")
list.forEach(item => {
google.script.run
.withSuccessHandler(() => {
newMessage(item.name + " has been copied")
})
.copyItem(item.id, targetId)
})
})
.getFolderItems()
})
.createTargetFolder(targetName)
}
</script>
</head>
<body>
<div>
<label for="newFolderName">Type in Name of new folder that contents will be copied to</label>
<input id="newFolderName" type="text">
<button type="button" onclick='copyFolder();' id="download">Copy</button>
</div>
<div id="messages">
-
</div>
</body>
</html>
Apps Script
class FileToCopy {
constructor(id, name) {
this.id = id;
this.name = name;
}
}
function doGet() {
return HtmlService.createHtmlOutputFromFile('form');
}
function createTargetFolder(name){
Logger.log(name)
var target = DriveApp.createFolder(name);
return target.getId()
}
function getFolderItems() {
var sourceFolderId = "[SOURCE_FOLDER_ID]";
var source = DriveApp.getFolderById(sourceFolderId);
var files = source.getFiles();
let output = []
while (files.hasNext()) {
var file = files.next();
let id = file.getId();
let name = file.getName();
let newItem = new FileToCopy(id,name)
output.push(newItem)
}
return output;
}
function copyItem(id, target) {
let file = DriveApp.getFileById(id)
let folder = DriveApp.getFolderById(target)
file.makeCopy(folder);
}
Result
Reference
Client to Server Communications

Google Calendar Authorization Apps Script - Standalone Web App

I am new to Apps Script and Web Development. I thought it would be nice to make a simple app to get started.
Goal: Display the future events of the user.
Problem: I am stuck on getting user authorization. Currently, the script is displaying my events. Instead, I want the script to display the user's (who is accessing the web app) events.
I found this sample from the documentation. This function gets the list of events of the user. https://developers.google.com/calendar/quickstart/apps-script
Then I wrote a basic index.html file to display the string populated by this above function to the user.
index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<script>
function getEventsOnClick() {
google.script.run.withSuccessHandler(changeDisplay).listAllEvents();
}
function changeDisplay(display) {
var div = document.getElementById('output');
div.innerHTML = display;
}
</script>
<div id="output"> Hello! </div>
<button onclick="getEventsOnClick()">Run Function</button>
</body>
</html>
code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index');
}
function listAllEvents() {
var calendarId = 'primary';
var now = new Date();
var display = ""
var events = Calendar.Events.list(calendarId, {
timeMin: now.toISOString(),
maxResults: 2500,
});
if (events.items && events.items.length > 0) {
for (var i = 0; i < events.items.length; i++) {
var event = events.items[i];
if (event.start.date) {
// All-day event.
var start = new Date(event.start.date);
var end = new Date(event.end.date);
display = display + 'Start: ' + start.toLocaleDateString() + '; End: ' + end.toLocaleDateString() + ". ";
} else {
var start = new Date(event.start.dateTime);
var end = new Date(event.end.dateTime);
display = display + 'Start: ' + start.toLocaleString() + '; End: ' + end.toLocaleString() + ". ";
}
}
} else {
display = 'No events found.';
}
Logger.log("%s", display)
return display
}
Again, nothing is wrong with the above code. It does display events as expected. The problem is that it is displaying my events rather than the user. So, if I give a user URL for the app, then I want this app to request authorization and display their event. How would I do that?
Thanks!
When you deploy the app, make sure you choose to execute as the user rather than as yourself. (as yourself is the default).

Upload file to Google Team Drive from php or html form

I am considering using either php or html form with an upload button to upload a file to a Google Team Drive.
I am wondering if anyone has had success using the Google Picker API? If so, would you kindly share your solution.
This is the info I'm referencing on Google API
Thank you for your help!
After hours of trial and error, I got it working.
My code is posted below.
Remember to set your URLs in OAuth settings
These are mine using MAMP:
http://localhost:8888
http://localhost:8888/printing-forms/custom-stationery
TEAM DRIVE ID is found in Team Drive URL
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title>Google File Upload for Custom Stationary Orders</title>
<script type="text/javascript">
// The Browser API key obtained from the Google API Console.
// Replace with your own Browser API key, or your own key.
var developerKey = '[your API key]';
// The Client ID obtained from the Google API Console. Replace with your own Client ID.
var clientId = "[your Client ID]"
// Replace with your own project number from console.developers.google.com.
// See "Project number" under "IAM & Admin" > "Settings"
var appId = "[your project number]";
// Scope to use to access user's Drive items.
var scope = ['https://www.googleapis.com/auth/drive.file'];
var pickerApiLoaded = false;
var oauthToken;
// Use the Google API Loader script to load the google.picker script.
function loadPicker() {
gapi.load('auth', {'callback': onAuthApiLoad});
gapi.load('picker', {'callback': onPickerApiLoad});
}
function onAuthApiLoad() {
window.gapi.auth.authorize(
{
'client_id': clientId,
'scope': scope,
'immediate': false
},
handleAuthResult);
}
function onPickerApiLoad() {
pickerApiLoaded = true;
createPicker();
}
function handleAuthResult(authResult) {
if (authResult && !authResult.error) {
oauthToken = authResult.access_token;
createPicker();
}
}
// Create and render a Picker object for searching images.
function createPicker() {
if (pickerApiLoaded && oauthToken) {
// var view = new google.picker.View(google.picker.ViewId.DOCS);
// view.setMimeTypes("pdf");
var picker = new google.picker.PickerBuilder()
.enableFeature(google.picker.Feature.SUPPORT_TEAM_DRIVES)
// Hide list of files on the Drive
.enableFeature(google.picker.Feature.MINE_ONLY)
// Hide the Upload option for Personal Drive
// .enableFeature (google.picker.Feature.NAV_HIDDEN)
.addView(new google.picker.DocsUploadView()
// Custom Stationary Folder
.setParent('[Team Drive ID from URL]'))
.addView(new google.picker.DocsView(google.picker.ViewId.DOCS)
.setEnableTeamDrives(true)
.setSelectFolderEnabled(false)
// Upload files to Custom Stationary Folder
.setParent('[Team Drive ID from URL]'))
// Select more than one file
// .enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
.setAppId(appId)
.setOAuthToken(oauthToken)
// Displays Personal Drive folder
// .addView(view)
.setDeveloperKey(developerKey)
.setCallback(pickerCallback)
.build();
picker.setVisible(true);
}
}
// A simple callback implementation.
function pickerCallback(data) {
if (data.action == google.picker.Action.PICKED) {
var fileId = data.docs[0].name;
// Show the ID of the Google Drive folder
document.getElementById('result').innerHTML = fileId + ' has been successfully uploaded';
// location.reload();
// alert('The user selected: ' + fileId);
}
}
</script>
</head>
<body>
<div id="result"></div>
<div id="continue">Back to the Order Form</div>
<!-- The Google API Loader script. -->
<script type="text/javascript" src="https://apis.google.com/js/api.js?onload=loadPicker"></script>
</body>
</html>

Direct to a new page after form submission

Currently I've developed a Google Scripts API which is used for people to upload files to a shared Google Drive folder. After the file are uploaded successfully, I want them to be taken to a separate "Thank you" page so it is clear their upload has worked. Currently I only have a message on the same page to this effect and I cannot figure out how to direct to a new page that I have created.
This is the additional bit I found from different questions to try direct to a new page however this is not working so far, as it remains on the same upload form page. I have included it at the bottom of my code.gs file. Any ideas on how to direct to a custom page that just says "thank you" or something similar would be great!
function doPost(e) {
var template = HtmlService.createTemplateFromFile('Thanks.html');
return template.evaluate();
}
The rest of my code is as follows:
Code.gs:
function doGet() {
return HtmlService.createHtmlOutputFromFile('form').setSandboxMode(
HtmlService.SandboxMode.IFRAME);
}
function createFolder(parentFolderId, folderName) {
try {
var parentFolder = DriveApp.getFolderById(parentFolderId);
var folders = parentFolder.getFoldersByName(folderName);
var folder;
if (folders.hasNext()) {
folder = folders.next();
} else {
folder = parentFolder.createFolder(folderName);
}
return {
'folderId' : folder.getId()
}
} catch (e) {
return {
'error' : e.toString()
}
}
}
function uploadFile(base64Data, fileName, folderId) {
try {
var splitBase = base64Data.split(','), type = splitBase[0].split(';')[0]
.replace('data:', '');
var byteCharacters = Utilities.base64Decode(splitBase[1]);
var ss = Utilities.newBlob(byteCharacters, type);
ss.setName(fileName);
var folder = DriveApp.getFolderById(folderId);
var files = folder.getFilesByName(fileName);
var file;
while (files.hasNext()) {
// delete existing files with the same name.
file = files.next();
folder.removeFile(file);
}
file = folder.createFile(ss);
return {
'folderId' : folderId,
'fileName' : file.getName()
};
} catch (e) {
return {
'error' : e.toString()
};
}
}
function doPost(e) {
var template = HtmlService.createTemplateFromFile('Thanks.html');
return template.evaluate();
}
Form.html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<div align="center">
<p><img src="https://drive.google.com/uc?export=download&id=0B1jx5BFambfiWDk1N1hoQnR5MGNELWRIM0YwZGVZNzRXcWZR"
height="140" width="400" ></p>
<div>
<form id="uploaderForm">
<label for="uploaderForm"> <b> Welcome to the Tesco's animal welfare and soy reporting system. </b> </label>
<BR>
<BR>
<div style="max-width:500px; word-wrap:break-word;">
Please add your contact information below and attach a copy of your company's animal welfare standard before clicking submit. Wait for the browser to confirm your submission and you may then close this page.
<BR>
<BR>
Thank you very much for your submission.
</div>
<BR>
<BR>
<div>
<input type="text" name="applicantName" id="applicantName"
placeholder="Your Name">
</div>
<BR>
<div>
<input type="text" name="applicantEmail" id="applicantEmail"
placeholder="Your Company">
</div>
<BR>
<BR>
<div>
<input type="file" name="filesToUpload" id="filesToUpload" multiple>
<input type="button" value="Submit" onclick="uploadFiles()">
</div>
</form>
<br>
<br>
<br>
<br>
<br>
<br>
<div id="output"></div>
<script>
var rootFolderId = '1-aYYuTczQzJpLQM3mEgOkWsibTak7KE_';
var numUploads = {};
numUploads.done = 0;
numUploads.total = 0;
// Upload the files into a folder in drive
// This is set to send them all to one folder (specificed in the .gs file)
function uploadFiles() {
var allFiles = document.getElementById('filesToUpload').files;
var applicantName = document.getElementById('applicantName').value;
if (!applicantName) {
window.alert('Missing applicant name!');
}
var applicantEmail = document.getElementById('applicantEmail').value;
if (!applicantEmail) {
window.alert('Missing applicant email!');
}
var folderName = applicantEmail;
if (allFiles.length == 0) {
window.alert('No file selected!');
} else {
numUploads.total = allFiles.length;
google.script.run.withSuccessHandler(function(r) {
// send files after the folder is created...
for (var i = 0; i < allFiles.length; i++) {
// Send each file at a time
uploadFile(allFiles[i], r.folderId);
}
}).createFolder(rootFolderId, folderName);
}
}
function uploadFile(file, folderId) {
var reader = new FileReader();
reader.onload = function(e) {
var content = reader.result;
document.getElementById('output').innerHTML = 'uploading '
+ file.name + '...';
//window.alert('uploading ' + file.name + '...');
google.script.run.withSuccessHandler(onFileUploaded)
.uploadFile(content, file.name, folderId);
}
reader.readAsDataURL(file);
}
function onFileUploaded(r) {
numUploads.done++;
document.getElementById('output').innerHTML = 'uploaded '
+ r.fileName + ' (' + numUploads.done + '/'
+ numUploads.total + ' files).';
if (numUploads.done == numUploads.total) {
document.getElementById('output').innerHTML = 'All of the '
+ numUploads.total + ' files are uploaded';
numUploads.done = 0;
}
}
</script>
<label for="uploaderForm">
Powered by 3Keel
</label>
</body>
</html>
Thanks.html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
Thank you for submitting!
</body>
</html>
EDIT:
I have changed this function as recommended:
if (numUploads.done == numUploads.total) {
window.location = 'Thanks.html';
numUploads.done = 0;
Now it is redirecting to another page but I am faced with this error:
That’s an error.
The requested URL was not found on this server. That’s all we know.
If you are looking for the solution of your issue yet, how about this answer?
You want to open Thanks.html when the process at Form.html is finished.
Form.html and Thanks.html are put in a project.
If my understanding is correct, how about this workaround? I have ever experienced with your situation. At that time, I could resolve this issue by this workaround.
Modification points:
It is not required to use doPost() to access to Thanks.html. I think that you can achieve what you want using doGet().
I think that #Scott Craig's answer can be also used for this situation. In my workaround, the URL of window.location = 'Thanks.html'; is modified.
Uses the URL of deployed Web Apps. In your script, when users access to your form, they access to the URL of the deployed Web Apps. In this workaround, it is used by adding a query parameter.
Modified scripts:
Form.html
For the script added in your question as "EDIT", please modify as follows.
From:
window.location = 'Thanks.html';
To:
window.location = 'https://script.google.com/macros/s/#####/exec?toThanks=true';
https://script.google.com/macros/s/#####/exec is the URL of the the deployed Web Apps. Please add a query parameter like toThanks=true. This is a sample query parameter.
Code.gs
Please modify doGet() as follows.
From:
function doGet() {
return HtmlService.createHtmlOutputFromFile('form')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
To:
function doGet(e) {
if (e.parameter.toThanks) {
return HtmlService.createHtmlOutputFromFile('Thanks')
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
} else {
return HtmlService.createHtmlOutputFromFile('form')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
}
Note:
When the script of the deployed Web Apps was modified, please redeploy it as new version. By this, the latest script is reflected to Web Apps.
The flow of this modified script is as follows.
When users access to the Form.html, because the query parameter of toThanks=true is not used, Form.html is returned.
When onFileUploaded() is run and if (numUploads.done == numUploads.total) {} is true, it opens the Web Apps URL with the query parameter of toThanks=true. By this, if (e.parameter.toThanks) {} of doGet() is true, and Thanks.html is returned and opened it.
In my environment, I could confirm that this modified script worked. But if this didn't work in your environment, I'm sorry. At that time, I would like to think of about the issue.
I might be misunderstanding your question, but from what I understand, instead of this line:
document.getElementById('output').innerHTML = 'All of the '
+ numUploads.total + ' files are uploaded';
You want to redirect to Thanks.html. If that's correct, just replace the above line with:
window.location = 'Thanks.html';

Automated download of file from Drive in Web App?

I'm trying to write a polling web app that checks Google Drive and automatically downloads files without user interaction.
Using ContentService I have managed to get things working when I place the code in the doGet function.
However this only works once and there does not appear to be a way to refresh or reload the page automatically on a timer event.
Using a SetTimeout on the client side javascript I can get a function on the server side to automatically trigger at certain intervals but then I am stuck with what to do with the output from ContentService.
The on Success call back will not accept the output from createTextOutput.
My solution does not not need to be deployed and I'm happy to execute from the editor if that expands my choices.
So once I have the output from createTextOutput on my server side what am I supposed to do with it to get it back to the client in order to cause the file download?
I have included the code if that helps.
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
setTimeout(
function ()
{
document.getElementById('results').innerHTML = 'Event Timer';
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(onFailure)
.fetchFromGoogleDrive();
}, 60000);
function onSuccess(sHTML)
{
document.getElementById('results').innerHTML = 'File Downloaded ' + sHTML;
}
function onFailure(error)
{
document.getElementById('results').innerHTML = error.message;
}
</script>
</head>
<body>
<div id="results">Waiting to DownLoad!</div>
id="Fetch">Fetch!</button>
</body>
</html>
function doGet() {
Logger.log('doGet');
return HtmlService.createHtmlOutputFromFile('form.html');
}
function fetchFromGoogleDrive() {
//Logger.Log('fetchFromGoogleDrive');
var fileslist = DriveApp.searchFiles("Title contains 'Expected File'");
if (fileslist.hasNext()) {
//Logger.Log('File found');
var afile = fileslist.next();
var aname = afile.getName();
var acontent = afile.getAs('text/plain').getDataAsString();
var output = ContentService.createTextOutput();
output.setMimeType(ContentService.MimeType.CSV);
output.setContent(acontent);
output.downloadAsFile(aname);
return afile.getDownloadUrl();
}
else
{
//Logger.Log('No File Found');
return 'Nothing to download';
}
//Logger.log('All files processed.');
}
EDIT: Different answer after clarification.
If this is intended to run automated as a webapp what I would do is return the getDownloadUrl and create a new iFrame using that that as the source.
Apps Script
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
function getDownloadLink(){
//slice removes last parameter gd=true. This needs to be removed. slice is a hack you should do something better
return DriveApp.getFileById("0B_j9_-NbJQQDckwxMHBzeVVuMHc").getDownloadUrl().slice(0,-8);
}
index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<p id="dlBox"></p>
</body>
<script>
function buildLink(res){
var dlBox = document.createElement("iframe");
dlBox.src = res;
document.getElementById("dlBox").appendChild(dlBox)
}
//automate this as you need
google.script.run
.withSuccessHandler(buildLink)
.getDownloadLink();
</script>
</html>