Malformed html error thrown from html based object in appscript - html

I've hit a wall on an issue when I try to display the content.text object in apps script, it returns a malformed html error specifically regarding my fetch to a GET request of
https://www.cloudconnect.xxx...
I get everything I need, but the content.text bit which is throwing a malformed html error in apps script. I'd like to use the html and return the documents as is with proper formatting and believe that I can properly parse this html to apps script using htmloutput as it needs to be sanitized, but I believe it's what's throwing the malformed html object. How can I proceed without escaping html characters? How can I properly parse this? Has anyone been successful at this by any chance?
Example of content.text:
<body>
<!-- [DocumentBodyStart:a63392fa-f859-4513-867e-1f3d2714b006] -->
<div class=\"jive-rendered-content\">
<p>Hi,team!</p>
<p style=\"min-height: 8pt; padding: 0px;\"> </p>
<p>When executing attest () of SafetyNet Attestation API, apkPackageName is obtained as a parameter.</p>
<p>I ran this API several times.</p>
<p>As a result, the apkPackageName parameter was missing only once.</p>
<p>In all other execution results, the parameter apkPackageName is present and will not occur again.</p>
<p style=\"min-height: 8pt; padding: 0px;\"> </p>
<p>Why can't I get the apkPackageName when running the SafetyNet Attestation API on a device that has not been
tampered with?</p>
<p style=\"min-height: 8pt; padding: 0px;\"> </p>
<p>device : Kyocera 704KC</p>
<p style=\"min-height: 8pt; padding: 0px;\"> </p>
<p>Regards,</p>
</div><!-- [DocumentBodyEnd:a63392fa-f859-4513-867e-1f3d2714b006] -->
</body>
Would anyone have any pointers on how to proceed from here? My goal is to obtain the text from the content.text object, which I can see on any regular editor, but not in apps script for some reason while using the html format that it returns as is.
Code.gs
function doGet(request) {
return HtmlService.createTemplateFromFile('Page').evaluate();
}
function include(filename) {
var finalRequest = UrlFetchApp.fetch('https://www.cloudconnect.xxx...');
var data = finalRequest.toString().replace("throw 'allowIllegalResourceCall is false.';", "").trim();
data = JSON.parse(data);
var returnedData = [];
for(var i in data.list){
var content = data.list[i];
var content_subject = JSON.stringify(content.subject);
var content_text = JSON.stringify(content.content.text);
returnedData.push(content_subject + "<br />" + "<br />" + textBody(content_text));
}
return returnedData;
}
function textBody(content){ // <-- where the error throws on the content_text object
return HtmlService.createHtmlOutput(content);
}
var entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
};
function escapeHtml(string) {
return String(string).replace(/[&<>"'`=\/]/g, function (s) {
return entityMap[s];
});
}
function myFunction() {
Logger.log(HtmlService
.createTemplateFromFile('Page')
.getCode());
}
Page.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<?!= include('Stylesheet'); ?>
</head>
<body>
<script>
var responseSubject;
var responseContent;
function displayData(responseSubject, responseContent) {
document.getElementById('output').innerHTML = responseSubject + <br> + responseContent + <br>;
}
google.script.run.withFailureHandler(displayData).withSuccessHandler(displayData).include();
</script>
</body>
</html>
Update
I have hit a wall returning the Exception: Cannot call SpreadsheetApp.getUi() from this context. (line 21, file "Code")
Code.gs
function doGet(request) {
return HtmlService.createTemplateFromFile('Page').evaluate();
}
function include(filename) {
var finalRequest = UrlFetchApp.fetch('https://www.cloudconnect.xxx....');
var data = finalRequest.toString().replace("throw 'allowIllegalResourceCall is false.';", "").trim();
data = JSON.parse(data);
var returnedData = [];
for(var i in data.list){
var content = data.list[i];
var contentSubject = JSON.stringify(content.subject);
var contentText = JSON.stringify(content.content.text);
returnedData.push(contentSubject + "<br/>" + "<br/>");
var fixedContent = escapeHtml(contentText);// fixes the malformed Html error
var ui = HtmlService.createHtmlOutput(fixedContent);//the attempt to read the onlick event and load the content text - but it throws the error: Exception: Cannot call SpreadsheetApp.getUi() from this context. (line 21, file "Code")
SpreadsheetApp.getUi().showModelessDialog(ui);
Logger.log("returnedData is: " + returnedData);
}
return returnedData;
}
var entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
};
function escapeHtml(string) {
return String(string).replace(/[&<>"'`=\/]/g, function (s) {
return entityMap[s];
});
}
function myFunction() {
Logger.log(HtmlService
.createTemplateFromFile('Page')
.getCode());
}
//function contentBody(responseContent){ <-- realized I can't do this from a custom function
//var html = responseContent;
//var ui = HtmlService.createHtmlOutput(html);
//SpreadsheetApp.getUi().showModelessDialog(ui);
//}
Page.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<?!= include('Stylesheet'); ?>
<script>
var responseSubject;
var responseContent;
function displaySubjects(responseSubject) {
document.getElementById('output').addEventListener('click', getContentBody).innerHTML = responseSubject;
}
google.script.run.withFailureHandler(displaySubjects).withSuccessHandler(displaySubjects).include();
//function displayContentText(responseContent){
//document.getElementById('projection').innerHTML = responseContent;
//}
//google.script.run.withFailureHandler(displayContentText).withSuccessHandler(displayContentText).contentBody();
</script>
</head>
<body>
<p id = "output"></p>
<p id = "projection"></p>
</body>
</html>
My goal here is to add a click listener to the subjects and have them load the content text through the Html service.
Any help would be highly appreciated please.
Cheers!

This works:
function htmltest() {
var html='<body><!--[DocumentBodyStart:a63392fa-f859-4513-867e-1f3d2714b006]--><div class="jive-rendered-content"><p>Hi,team!</p><p style="min-height:8pt;padding:0px;"> </p><p>When executing at test() of Safety NetAttestationAPI, apkPackageName is obtained as a parameter.</p><p>I ran this API several times.</p><p>As a result,the apkPackageName parameter was missing only once.</p><p>In all other execution results,the parameter apkPackageName is present and will not occur again.</p><p style="min-height:8pt;padding:0px;"> </p><p>Whycan\'t I get the apkPackageName when running the Safety NetAttestation API on a device that has not been tampered with?</p><p style="min-height:8pt;padding:0px;"> </p><p>device:Kyocera704KC</p><p style="min-height:8pt;padding:0px;"> </p><p>Regards,</p></div><!--[DocumentBodyEnd:a63392fa-f859-4513-867e-1f3d2714b006]--></body>';
var ui=HtmlService.createHtmlOutput(html).setHeight(500);
SpreadsheetApp.getUi().showModelessDialog(ui, "HTML Test");
}
Here's what I get when I run it just that way it is.

Related

Google App Script: how to display google drive files with links to templated HTML

I'm new in both Google App Scripting and JavaScript. So, I'm trying to display the grabbed files from my Google Drive with links however when running displayData(); it's literally showing the link and the title of the file on the page without the actual link in it. Here's picture of the html
output.
Here's what I have so far:
Code.gs
function doGet() {
var output = HtmlService.createTemplateFromFile('Page').evaluate();
return output;
}
function include(filename){
return HtmlService.createHtmlOutputFromFile(filename)
.getContent();
}
function displayData() {
var dir = 'Somefoldername';
var foldername = DriveApp.getFoldersByName(dir).next();
var foldercont = foldername.getFiles();
var listicon = '<img src="https://drive-thirdparty.googleusercontent.com/16/type/application/vnd.google-apps.document">';
var file, title, links, list = [];
while (contents.hasNext()) {
file = foldercont.next();
title = file.getName();
links = file.getUrl();
date = file.getDateCreated();
list.push('<tr><td>' + listicon + '<a href ="' + links + '">' + title +'</td></tr>');
}
return list;
Page.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<?!= include('Stylesheet'); ?>
</head>
<body>
<div class="doclist">
<?= displayData(); ?>
</div>
</body>
</html>
How would I display the Google Drive file hyperlinks in the html template?
Thank you.
Here is a Sample Code:
Note:
I temporarily removed <?!= include('Stylesheet'); ?> in the html file since it is not defined.
Code.gs
function doGet() {
var output = HtmlService.createTemplateFromFile('Page').evaluate();
return output;
}
function include(filename){
return HtmlService.createHtmlOutputFromFile(filename)
.getContent();
}
function displayData() {
var dir = 'Somefoldername';
var foldername = DriveApp.getFoldersByName(dir).next();
var foldercont = foldername.getFiles();
var listicon = '<img src="https://drive-thirdparty.googleusercontent.com/16/type/application/vnd.google-apps.document">';
var file, title, links, list = [];
while (foldercont.hasNext()) {
file = foldercont.next();
title = file.getName();
links = file.getUrl();
date = file.getDateCreated();
list.push('<tr><td>' + listicon + '<a href ="' + links + '">' + title +'</td></tr>');
}
return list.join(' ');
}
Modifications done:
Replace while (contents.hasNext()) with while (foldercont.hasNext())
Combine your array list into a single string using array.join(' ') with spaces as its separator
Page.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<script>
google.script.run.withSuccessHandler(function(tblStr){
document.getElementById('FileList').innerHTML = tblStr;
}).displayData();
</script>
<table id="FileList">
</table>
</body>
</html>
Modifications done:
I added a table in the html body with the id "FileList"
I called the server-side function displayData() using google.script.run.withSuccessHandler(function).displayData(). The return value of displayData() will be passed to the callback function's first parameter.
I updated the table's content based on the displayData()'s return value using this procedure document.getElementById('FileList').innerHTML = tblStr;
Output:
Some observations:
The function is returning an array list = [], and you are pushing data into that array.
Your HTML in the screenshot has stray commas in it between each item: ...</td></tr>,<tr><td>....
You shouldn't place a <div> inside a table.
Both of these suggest that you should be appending your data to a string variable, instead of pushing data into an array.
Then return that string from your function, instead of the array.
The string variable will contain the entire contents of your HTML rows and columns.
For the <div>, remove it and place the class in the body:
<body class="doclist">
Depending on how your CSS is set up, that may need modifying to accommodate this change.
Final suggestion: take the resulting HTML which is generated and run it through a validator - for example: https://validator.w3.org/#validate_by_input
That may find some additional issues which need correcting.

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';

Holding value in textarea for Google Script

I'm trying to write code that will help some of my users take a JSON response and convert it to a table in Google Sheets. I have the code for the JSON to Table script, courtesy of Amit Agarwal at www.ctrlq.org.
The way that I'm trying to do is that Google Sheets will generate a pop up for my user to copy and paste the JSON straight into it and then it will pass the object to the JSON to Table code. I'm having a problem figuring out how to actually do that.
Code.gs
function showPrompt()
{
var ui = SpreadsheetApp.getUi(); // Same variations.
var html = HtmlService.createHtmlOutputFromFile('Upload');
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
.showModalDialog(html, 'Dialog title');
}
function setJSON(json)
{
var json = json; //json object?
return json;
}
// Written by Amit Agarwal www.ctrlq.org
function writeJSONtoSheet(json) {
var sheet = SpreadsheetApp.getActiveSheet()[1];
var keys = Object.keys(json).sort();
var last = sheet.getLastColumn();
var header = sheet.getRange(1, 1, 1, last).getValues()[0];
var newCols = [];
for (var k = 0; k < keys.length; k++) {
if (header.indexOf(keys[k]) === -1) {
newCols.push(keys[k]);
}
}
if (newCols.length > 0) {
sheet.insertColumnsAfter(last, newCols.length);
sheet.getRange(1, last + 1, 1, newCols.length).setValues([newCols]);
header = header.concat(newCols);
}
var row = [];
for (var h = 0; h < header.length; h++) {
row.push(header[h] in json ? json[header[h]] : "");
}
sheet.appendRow(row);
}
Dialog box HTML:
<!DOCTYPE html>
<html>
<head>
<script>
function setJSON() {
// how do i add value to set to set JSON
var json = ""
google.script.run.setJSON(json);
}
</script>
<base target="_top">
</head>
<body>
text text
<textarea rows="20" cols="20" placeholder="Paste your Data here. Do not format. Do not worry if it looks weird." name="json">DATA</textarea>
<input type="button" class="button" value="Submit JSON" onclick="setJSON()">
</body>
</html>
Thank you in advance!
Here's a simple example of triggering a dialog via an in-sheet menu and passing the input back to a server side function.
Code.gs
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Input Form Test')
.addItem('Input Form', 'showInputForm')
.addToUi();
}
function showInputForm() {
var html = HtmlService.createHtmlOutputFromFile('inputform');
SpreadsheetApp.getUi().showModalDialog(html, 'Input Form Test');
}
function logInput(input) {
Logger.log(input)
}
inputform.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<script>
function setJSON(form) {
var jsonData = document.getElementById('json-data');
google.script.run.logInput(jsonData.value);
google.script.host.close();
}
</script>
<body>
<textarea id="json-data" rows="20" cols="20" placeholder="Paste your data here. Do not format. Do not worry if it looks weird." name="json">DATA</textarea>
<input type="button" class="button" value="Submit JSON" onclick="setJSON()">
</body>
</html>
Logging Output
After clicking the "Submit JSON" button with "test input" entered into the form, this is the output in the script logs:
[18-06-15 13:47:15:741 PDT] test input
Notes
I added an id attribute to the textarea element
the id is used to fetch the element programmatically
you may want to perform some client-side validations in setJSON(). For example:
verify that the value is not still the default value (i.e. DATA)
validate that the JSON data can be parsed succesfully (i.e. JSON.parse())
an example implementation with the above validations:
function setJSON(form) {
var jsonData = document.getElementById('json-data');
if (jsonData.value == 'DATA') {
alert("Please enter JSON data into the form.");
return;
}
try {
JSON.parse(jsonData.value)
}
catch(e) {
console.log(e); // Outputs to the browser console.
alert("Unable to parse JSON data.");
return;
}
google.script.run.logInput(jsonData.value);
google.script.host.close();
}
to fit your use case, in inputform.html you'd replace the logInput() call with either:
a function that calls JSON.parse() and then calls writeJSONtoSheet() with the result
writeJSONtoSheet() if you update it to expect text and add a JSON.parse() call at the beginning of the function

XMLHttpRequest error for the Viewer

I use the following HTML file to test the Headless Viewer of Autodesk Forge. The test url will look like:
http://localhost:8080/HeadlessViewer.html?token={{Bearer}}&urn={{base64URN}}
The token has scope=data:read, urn is base64 format.
<html>
<head>
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=no" />
</head>
<body>
<div id="MyViewerDiv"></div>
<!-- The Viewer JS -->
<script src="https://developer.api.autodesk.com/viewingservice/v1/viewers/three.min.js?v=v2.10.*"></script>
<script src="https://developer.api.autodesk.com/viewingservice/v1/viewers/viewer3D.js?v=v2.10.*"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<!-- Developer JS -->
<script>
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
function initViewer(token, urn) {
var viewerApp;
var options = {
env: 'AutodeskProduction',
accessToken: token
};
var documentId = atob(urn); // 'urn:<YOUR_URN_ID>';
Autodesk.Viewing.Initializer(options, onInitialized);
function onInitialized() {
viewerApp = new Autodesk.Viewing.ViewingApplication('MyViewerDiv');
viewerApp.registerViewer(viewerApp.k3D, Autodesk.Viewing.Viewer3D);
viewerApp.loadDocument(documentId, onDocumentLoaded);
}
function onDocumentLoaded(lmvDoc) {
var modelNodes = viewerApp.bubble.search(av.BubbleNode.MODEL_NODE); // 3D designs
var sheetNodes = viewerApp.bubble.search(av.BubbleNode.SHEET_NODE); // 2D designs
var allNodes = modelNodes.concat(sheetNodes);
if (allNodes.length) {
viewerApp.selectItem(allNodes[0].data);
if (allNodes.length === 1) {
alert('This tutorial works best with documents with more than one viewable!');
}
} else {
alert('There are no viewables for the provided URN!');
}
}
}
$(document).ready(function () {
var url = window.location.href,
token = getParameterByName('token', url),
urn = getParameterByName('urn', url);
initViewer(token, urn);
});
</script>
</body>
</html>
However, it stops at the exception XMLHttpRequest.responseText. Please see the attached image: Error image
I tried your code just replacing the "accessToken: <>" and "var documentId = <>" and worked fine. Looking at your code, I believe the problem may be at the following line:
var documentId = atob(urn); // 'urn:<YOUR_URN_ID>';
The atob function will decode the string, which means it will not be on Base64. But the documentId should be like:
var documentId = 'urn:c29tZSByYW5kb20gd29yZHMgaGVyZQ==';
Please make sure the documentId is properly formed.
Finally, note the Viewer requires URL Safe encoding. Consider encoding on server (safer to transmit) or doing it on client-side, see this answer.

How to pass a parameter to html?

I have a script that uses the file picker but I need to pass a specific parameter which is called userId and is kept as a global variable in the calling script. As the calls are asynchronous it seems I cannot access this parameter. Is there a way to access the parameter from the html file or pass this parameter to the html?
I might be mixing templated html and non templated.
Here is the calling code (initiated through a menu item in a spreadsheet):
function syncStudentsFile(userId, ss) {
scriptUser_(userId); // save userId
Logger.log('SRSConnect : syncStudentsFile : userId:'+userId); // userId is correct here
var ss = SpreadsheetApp.getActiveSpreadsheet();
var html = HtmlService.createHtmlOutputFromFile('PickerSync.html')
.setWidth(600).setHeight(425);
SpreadsheetApp.getUi().showModalDialog(html, 'Select a file');
}
function scriptUser_(userId) {
if (userId !== undefined)
sUserId = userId; // Global variable
try { return sUserId; } catch (e) { return undefined; }
}
function getOAuthToken() { // used by Picker
DriveApp.getRootFolder();
return ScriptApp.getOAuthToken();
}
Here is the html picker file:
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<script type="text/javascript">
var DEVELOPER_KEY = '..............';
var DIALOG_DIMENSIONS = {width: 600, height: 425};
var pickerApiLoaded = false;
/**
* Loads the Google Picker API.
*/
gapi.load('picker', {'callback': function() {
pickerApiLoaded = true;
}});
/**
* Gets the user's access token from the server-side script so that
* it can be passed to Picker. This technique keeps Picker from needing to
* show its own authorization dialog, but is only possible if the OAuth scope
* that Picker needs is available in Apps Script. Otherwise, your Picker code
* will need to declare its own OAuth scopes.
*/
function getOAuthToken() {
google.script.run.withSuccessHandler(createPicker)
.withFailureHandler(showError).getOAuthToken();
}
/**
* Creates a Picker that can access the user's spreadsheets. This function
* uses advanced options to hide the Picker's left navigation panel and
* default title bar.
*
* #param {string} token An OAuth 2.0 access token that lets Picker access the
* file type specified in the addView call.
*/
function createPicker(token) {
if (pickerApiLoaded && token) {
var uploadView = new google.picker.DocsUploadView();
var picker = new google.picker.PickerBuilder()
// Instruct Picker to display only spreadsheets in Drive. For other
// views, see https://developers.google.com/picker/docs/#otherviews
.addView(google.picker.ViewId.DOCS)
.addView(google.picker.ViewId.RECENTLY_PICKED)
.addView(uploadView)
.hideTitleBar()
.setOAuthToken(token)
.setDeveloperKey(DEVELOPER_KEY)
.setCallback(pickerCallback)
// Instruct Picker to fill the dialog, minus 2 pixels for the border.
.setSize(DIALOG_DIMENSIONS.width - 2,
DIALOG_DIMENSIONS.height - 2)
.build();
picker.setVisible(true);
} else {
showError('Unable to load the file picker.');
}
}
/**
* A callback function that extracts the chosen document's metadata from the
* response object. For details on the response object, see
* https://developers.google.com/picker/docs/result
*
* #param {object} data The response object.
*/
function pickerCallback(data) {
var action = data[google.picker.Response.ACTION];
if (action == google.picker.Action.PICKED) {
var doc = data[google.picker.Response.DOCUMENTS][0];
var id = doc[google.picker.Document.ID];
google.script.host.close();
// --------------> user global parameter sUserId set earlier
google.script.run.PickerSyncFile(sUserId, id);
} else if (action == google.picker.Action.CANCEL) {
google.script.host.close();
}
}
/**
* Displays an error message within the #result element.
*
* #param {string} message The error message to display.
*/
function showError(message) {
document.getElementById('result').innerHTML = 'Error: ' + message;
}
</script>
<div>
<script>getOAuthToken()</script>
<p id='result'></p>
<input type="button" value="Close" onclick="google.script.host.close()" />
</div>
Here is the picker code:
function pickerSyncFile(userId, id) {
Logger.log('userId:'+userId); // BUG: it is null
Logger.log('id:'+id); // id returned well from picker
// rest of code here but userId was is incorrect
}
The safest way is to pass the needed data to the HTML directly. If you use properties or cache service it can get complex or fail under multiple simultaneous users.
There are many techniques to pass an initial object from the server (.gs) to the client (.html).
Using HtmlTemplate, you may do:
//.gs file
function doGet() {
var htmlTemplate = HtmlService.createTemplateFromFile('template-client');
htmlTemplate.dataFromServerTemplate = { first: "hello", last: "world" };
var htmlOutput = htmlTemplate.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setTitle('sample');
return htmlOutput;
}
and in your template-client.html file:
<!DOCTYPE html>
<script>
var data = <?!= JSON.stringify(dataFromServerTemplate) ?>; //Stores the data directly in the javascript code
// sample usage
function initialize() {
document.getElementById("myTitle").innerText = data.first + " - " + data.last;
//or use jquery: $("#myTitle").text(data.first + " - " + data.last);
}
// use onload or use jquery to call your initialization after the document loads
window.onload = initialize;
</script>
<html>
<body>
<H2 id="myTitle"></H2>
</body>
</html>
It is also possible to do it without using templating, by appending a hidden div to an HtmlOutput:
//.gs file:
function appendDataToHtmlOutput(data, htmlOutput, idData) {
if (!idData)
idData = "mydata_htmlservice";
// data is encoded after stringifying to guarantee a safe string that will never conflict with the html.
// downside: increases the storage size by about 30%. If that is a concern (when passing huge objects) you may use base94
// or even base128 encoding but that requires more code and can have issues, see http://stackoverflow.com/questions/6008047/why-dont-people-use-base128
var strAppend = "<div id='" + idData + "' style='display:none;'>" + Utilities.base64Encode(JSON.stringify(data)) + "</div>";
return htmlOutput.append(strAppend);
}
// sample usage:
function doGet() {
var htmlOutput = HtmlService.createHtmlOutputFromFile('html-sample')
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setTitle('sample');
// data can be any (serializable) javascript object.
// if your data is a native value (like a single number) pass an object like {num:myNumber}
var data = { first: "hello", last: "world" };
// appendDataToHtmlOutput modifies the html and returns the same htmlOutput object
return appendDataToHtmlOutput(data, htmlOutput);
}
and in your output-client.html:
<!DOCTYPE html>
<script>
/**
* getDataFromHtml
*
* Inputs
* idData: optional. id for the data element. defaults to "mydata_htmlservice"
*
* Returns
* The stored data object
*/
function getDataFromHtml(idData) {
if (!idData)
idData = "mydata_htmlservice";
var dataEncoded = document.getElementById(idData).innerHTML;
var data = JSON.parse(atob(dataEncoded));
return data;
}
// sample usage of getDataFromHtml
function initialize() {
var data = getDataFromHtml();
document.getElementById("myTitle").innerText = data.first + " - " + data.last;
//or use jquery: $("#myTitle").text(data.first + " - " + data.last);
}
// use onload or use jquery to call your initialization after the document loads
window.onload = initialize;
</script>
<html>
<body>
<H2 id="myTitle"></H2>
</body>
</html>
Both methods are compared and better explained in this little github I made:
https://github.com/zmandel/htmlService-get-set-data
I often use HtmlService templates to push static values to the client.
index.html
<script>
var domain = "<?=domain?>";
</script>
code.gs
var ui = HtmlService.createTemplateFromFile('Sidebar');
ui.domain = domain;
return ui.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME).setTitle(strings[lang][domain]);
In your code:
function scriptUser_(userId) {
if (userId !== undefined)
sUserId = userId; // Global variable
try { return sUserId; } catch (e) { return undefined; }
}
You are assigning a value to the global variable named sUserId. But, then when you try to retrieve it, nothing is there. Global variables loose their value as soon as the current instance of the code bring run is completed. Global variable don't persist their values.
You'll need to use the Properties Service to store the value. Or you could use the Cache service. If you want the value of the user id to expire after some time, use cache service.
By Appending to the HTML File, as shown below.
within Code.gs
function showDialog() {
var html = HtmlService.createHtmlOutputFromFile('html-sample')
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setWidth(600)
.setHeight(425);
var data = "Hello World!";
var strAppend = "<div id='id_for_div' style='display:none;'>" + data + "</div>";
html.append(strAppend);
var title = "Demo";
SpreadsheetApp.getUi().showModalDialog(html, title); // or DocumentApp or SlidesApp or FormApp.
}
html-sample.html
<!DOCTYPE html>
<html>
<head>
<script>
function setText(text) {
var element = document.getElementById("myid");
element.innerHTML = text;
}
function getDataFromHtml() {
var id = "id_for_div";
var dataEncoded = document.getElementById(id).innerHTML;
setText(dataEncoded);
}
</script>
</head>
<body>
<h1 id="myid">Sample Text</h1>
<button onclick="getDataFromHtml()">Try Now!</button>
</body>
</html>
Click the 'Try Now!' Button and See the Magic!
This post provides the solution how to pass parameter to a templated html:
html = HtmlService.createTemplateFromFile('page2');
html.id = s1;
Then in page 2 use the the tag to print out the id value;
<div class="info" >
<span id="ID" name="ID"><?=id?></span>
</div>