CORS block: WebApp POST to Sheet - javascript

I am getting an error due to CORS policy. It seems a common issue. I have read other questions and tried some of their solutions, but it still does not work.
My app does the following:
1- GET a list of values from a google sheet and show them in a drop down list in a web app (still using script.google.com)
2- POST the value that the user select in the drop down list from the web app to the sheet.
1 (GET) was working fine with no issues. When I added 2 (POST), it gave me the CORS error for the POST call. The GET seems fine.
Here is the code for POST:
function doPost(e) {
var value = JSON.parse(e.postData.contents).value;
var ss = SpreadsheetApp.openById('1Zbvy1x_DsBlcwK4FdjoY4m0MFvS_tYZlGtKvD36fDyk');
var sh = ss.getSheetByName('Dashboard');
sh.getRange(92, 2).setValue(value);
return ContentService.createTextOutput(JSON.stringify({message: "ok"})).setMimeType(ContentService.MimeType.JSON);
}
with HTML file:
<script>
function listQ() {
const index = this.selectedIndex;
if (index > 0) {
const e = document.getElementById("sel1");
const value = e.options[index].value;
const url = "https://script.google.com/a/google.com/macros/s/AKfycbxHX7cthji076OrqfY9ZpGa7jNDxKHUMf_ib7Ekmoo0Ir5DQF1Y/exec";
fetch(url, {
method: "POST",
body: JSON.stringify({index: index, value: value}),
})
.then(function (response) {
return response.json();
})
.then(function (data) {
console.log(data);
})
}
}
document.getElementById("sel1").addEventListener("change",listQ);
</script>
The GET part is fine, so no need to add the code here.
I have tried to change POST with PUT as suggested here: Change Google Sheet data based on user selection in Google Site (Web App) but it gave the same error.
I have also read this link, but mine is a POST request, not GET and I am not sure how to apply it to my case: CORS authorization on google sheets API requests
FYI The "origin" in the CORS error message is ...googleusercontent.com
UPDATE: I have also tried this: Google Apps Script cross-domain requests stopped working
I think I should implement that solution, but when I tried to add those mods in my code and it did not work. Probably I am adding the mods incorrectly since I am not 100% sure what I am doing. I get the error that the call back function is not defined. Here is what I did:
function doPost(e) {
var callback = e.parameter.callback;
var value = JSON.parse(e.postData.contents).value;
var ss = SpreadsheetApp.openById('1ROvDcIQ8JCGvvLvCvTKIqSor530Uj9ZJv-n6hQ761XA');
var sh = ss.getSheetByName('Dashboard');
sh.getRange(92, 2).setValue(value);
return ContentService.createTextOutput(callback+'('+JSON.stringify({message: "ok"})+')').setMimeType(ContentService.MimeType.JSON);
}
and in HTML side, I just modified the URL:
const url = "https://script.google.com/a/google.com/macros/s/AKfycbxHX7cthji076OrqfY9ZpGa7jNDxKHUMf_ib7Ekmoo0Ir5DQF1Y/exec?offset="+offset+"&baseDate="+baseDate+"&callback=?";

In Apps Script Web Apps, in order to access server-side functions from your current project, you don't need to make a GET or POST request to the Web App URL. You just need to use google.script.run.
In your exact case, you could do the following:
function listQ() {
const index = this.selectedIndex;
if (index > 0) {
const e = document.getElementById("sel1");
const value = e.options[index].value;
const body = { index: index, value: value };
google.script.run.withSuccessHandler(yourCallback).yourServerSideFunc(body);
}
}
function yourCallBack(response) {
// Whatever you want to do with the data retrieved from server-side
}
In the code above, the client-side function calls a server side function called yourServerSideFunc via google.script.run. If the server-side function returns successfully, a callback function is called (function yourCallback), whose purpose is to handle the data returned from the server (a callback is needed since, on its own, the function called by google.script.run returns void). This callback function receives the content returned by the server-side function as an argument.
And in the server-side, no need to use doPost, since you would not be making a POST request:
function yourServerSideFunc(body) {
var value = body["value"];
var ss = SpreadsheetApp.openById('1Zbvy1x_DsBlcwK4FdjoY4m0MFvS_tYZlGtKvD36fDyk');
var sh = ss.getSheetByName('Dashboard');
sh.getRange(92, 2).setValue(value);
return ContentService.createTextOutput(JSON.stringify({message: "ok"})).setMimeType(ContentService.MimeType.JSON);
}
Reference:
HTML Service: Communicate with Server Functions
Class google.script.run (Client-side API)

Related

Accessing a private Google sheet with a google cloud service account

I have a private Google Spreadsheet and I’m trying to access it programmatically using Google Visualization/Google Charts. I've created the service account, and I have tried using the google-auth-library and googleapis npm packages to create an access token that I can then use to access the spreadsheet. But, when I try to use that access token to read from the spreadsheet, the request fails with HTTP code 401 (Unauthorized). What do I need to do in order to make this work?
This is my code:
const { auth } = require('google-auth-library');
const keys = require('./jwt.keys.json');
const id = '{Spreadsheet ID}';
const sheetId = '{Sheet ID}';
const query = "{Query}";
async function getClient(){
const client = auth.fromJSON(keys);
client.scopes = ['https://www.googleapis.com/auth/spreadsheets.readonly'];
console.log(client);
return client;
}
async function main(){
const client = await getClient();
const token = await client.getAccessToken();
console.log(token);
console.log(token.token);
const url = `https://docs.google.com/spreadsheets/d/${id}/gviz/tq?tqx=out:csv&tq=${encodeURIComponent(query)}&access_token=${token.token}#gid=${sheetId}`;
const res = await client.request({url});
console.log(res);
}
main().catch(console.error);
When I saw your script, I thought that it is required modifying the scope. I had got the same situation as you (the status code 401 using your endpoint). Unfortunately, it seems that your endpoint cannot be used using the scope of https://www.googleapis.com/auth/spreadsheets.readonly. So, for example, how about changing it as follows, and testing it again?
From:
https://www.googleapis.com/auth/spreadsheets.readonly
To:
https://www.googleapis.com/auth/spreadsheets
or
https://www.googleapis.com/auth/drive.readonly
Note:
When I tested your endpoint using the modified scope, I confirmed that no error occurred. But if you tested it and when an error occurs, please check other part, again.

How to pass dynamic values to TwiML

I have an ASP.Net Core website in which I want to start an outbound call on the number provided on the page. For this, first I generate a token from the backend controller as:
var accountSid = ConfigurationManager.AppSettings["TwilioAccountSid"];
var authToken = ConfigurationManager.AppSettings["TwilioAuthToken"];
var appSid = ConfigurationManager.AppSettings["TwilioTwimlAppSid"];
var scopes = new HashSet<IScope>
{
{ new OutgoingClientScope(appSid) }
};
var capability = new ClientCapability(accountSid, authToken, scopes: scopes);
return capability.ToJwt();
Now on the page, once anyone enters the phone number and clicks on call, I initiate the call with the javascript function Twilio.Device.connect().
The TwiML app whose AppSID I am using while generating token has the Request URL which provides following TwiML:
<Response>
<Dial callerId="+1XXXYYY">+91XXXAAA</Dial>
</Response>
This works fine and I am able to receive call on the given hardcoded number in the TwiML. However I want to call on the number entered on the page.
So my question is how can I pass the numbered entered on the page(alongwith the CallerID as well) to this TwiML so that it dials to the specific entered number and not the hardcoded one?
If this is not possible then is there any other alternative available for my architecture?
Was able to finally start the call. For this, I used params in javascript method:
var params = {
phoneToCall: document.getElementById('phone-number').value,
callerIdToShow: document.getElementById('callerId').value
};
Twilio.Device.connect(params);
Then on the URL which returns the TwiML to dial, I received these params and used them in the xml as:
[HttpGet]
public HttpResponseMessage GetTwiML(string phoneToCall, string callerIdToShow)
{
string XML = $"<Response><Dial callerId=\"{callerIdToShow}\">{phoneToCall}</Dial></Response>";
return new HttpResponseMessage()
{
Content = new StringContent(XML, Encoding.UTF8, "application/xml")
};
}

preparing an input table on google tag manager server side to be send as part of an API request body

I am using Google Tag Manager Server Container.
I am trying to make a POST API call to
https://api.hubapi.com/events/v3/send?hapikey={{hubspot api key}}
The parts in between {{ }} are variables taken and set within google tag manager. They are populating properly.
The purpose of this API call is to send a custom behaviour event (similar to google analytics event) to Hubspot - custom behaviour events manual tracking
For these events, you can send in properties like page url, google cid, file type, page title etc.
These properties are added to the request body, so they are included in the setBody in the following request
sendHttpRequest(url, (statusCode, headers, body) => {
if (statusCode >= 200 && statusCode <300) {
data.gtmOnSuccess();
} else {
data.gtmOnFailure();
}
setResponseStatus(statusCode);
setResponseHeader('cache-control', headers['cache-control']);
setResponseBody(body);
},{headers: postHeaders, method: 'POST', timeout: 3000}, setBody);
setBody is set with the following code:
if (data.additionalProperties) {
for (let key in data.additionalProperties) {
postPropertiesData = ["{",postPropertiesData, data.additionalProperties[key].property,":",data.additionalProperties[key].value,"}"].join('');
}
}
const postProperties = JSON.stringify(postPropertiesData);
const setProperties = JSON.stringify({
utk: data.userToken,
eventName: data.eventName,
ocurredAt: getTimestamp(),
});
const setBody = [setProperties,'properties:',postProperties].join(' ');
the (if (data.additionalProperties) section takes the properties from an input table like this
.
data.additionalProperties access the table. The left column has the internal name called property and the right column has the internal name called value.
I need it to output as an object to 'properties': {'firstname':'michael','lastname':'stone','email':'michael.stone#gmail.com'} and then stringified using JSON.stringify() function
I used the code above to try prepare to be part of the request body.
I have also tried.
let postPropertiesData = {};
if (data.additionalProperties) {
for (let key in data.additionalProperties) {
postPropertiesData[data.additionalProperties[key].property =data.additionalProperties[key].value,"};
}
}
const setBody = JSON.stringify({
utk: data.userToken,
eventName: data.eventName,
properties: postPropertiesData,
ocurredAt: getTimestamp(),
});
either way I get the same result
the properties part is empty.
How do I resolve this?
You can also check the source of this HubSpot GTM Server Side tag https://github.com/stape-io/hubspot-tag
It also has functionality to send custom behaviour events to HubSpot.
And a blog post about how to use it is here https://stape.io/how-to-connect-website-with-hubspot-using-server-side-tracking/
in the end, I found the answer. I had to use the makeTableMap API
so I did const makeTableMap = require('makeTableMap');
and then instead of the if statements and the for, I used a ternary within the object itself.
const setBody = JSON.stringify({
'portalId':data.portalID,
'utk': data.userToken,
'eventName': data.eventName,
'properties': (data.additionalProperties) ? makeTableMap(data.additionalProperties,'property','value'):{},
'ocurredAt': getTimestamp(),
});
and that worked.

Zapier.com zipcode autofill city and state

We are using zapier.com to connect many programs, but one function that I need is to autofill the city and state from a zip code. This is available in zapier.com as setup Code by Zapier Run Javascript. I can't seem to figure this out and any help is much appreciated.
<script type="text/javascript">//<![CDATA[
$(function() {
// IMPORTANT: Fill in your client key
var clientKey; // Deleted for Stack Overflow
var cache = {};
var container = $("#example1");
var errorDiv = container.find("div.text-error");
/** Handle successful response */
function handleResp(data)
{
// Check for error
if (data.error_msg)
errorDiv.text(data.error_msg);
else if ("city" in data)
{
// Set city and state
container.find("input[name='city']").val(data.city);
container.find("input[name='state']").val(data.state);
}
}
// Set up event handlers
container.find("input[name='zipcode']").on("keyup change", function() {
// Get zip code
var zipcode = $(this).val().substring(0, 5);
if (zipcode.length == 5 && /^[0-9]+$/.test(zipcode))
{
// Clear error
errorDiv.empty();
// Check cache
if (zipcode in cache)
{
handleResp(cache[zipcode]);
}
else
{
// Build url
var url = "https://www.zipcodeapi.com/rest/"+clientKey+"/info.json/" + zipcode + "/radians";
// Make AJAX request
$.ajax({
"url": url,
"dataType": "json"
}).done(function(data) {
handleResp(data);
// Store in cache
cache[zipcode] = data;
}).fail(function(data) {
if (data.responseText && (json = $.parseJSON(data.responseText)))
{
// Store in cache
cache[zipcode] = json;
// Check for error
if (json.error_msg)
errorDiv.text(json.error_msg);
}
else
errorDiv.text('Request failed.');
});
}
}
}).trigger("change");
});
//]]></script>
It looks like you're trying to use client-side JavaScript here. This won't work in a Zapier code step because it's meant to be used in a browser (on a webpage). To make an HTTP request in a Zapier code step, you'll want to use fetch (here's some documentation on that).
Alternatively, the simplest way to get the data you need from that API is with a Webhook step:
Add a step to your Zap
Choose Webhooks by Zapier and select the GET action
Set up that step like this. The step will return city/state data which you can use in subsequent steps
There are a couple points of confusion here, the main one being:
Code by Zapier is not run "in the browser" (there is no <script> tags or Jquery) - it is run in something called Node.js.
You'll need to approach the problem completely differently as a result - definitely take a look at the samples found in https://zapier.com/help/code/ and at the Node docs https://nodejs.org/docs/v4.3.2/api/.

Get Cloudflare's HTTP_CF_IPCOUNTRY header with javascript?

There are many SO questions how to get http headers with javascript, but for some reason they don't show up HTTP_CF_IPCOUNTRY header.
If I try to do with php echo $_SERVER["HTTP_CF_IPCOUNTRY"];, it works, so CF is working just fine.
Is it possible to get this header with javascript?
#Quentin's answer stands correct and holds true for any javascript client trying to access server header's.
However, since this question is specific to Cloudlfare and specific to getting the 2 letter country ISO normally in the HTTP_CF_IPCOUNTRY header, I believe I have a work-around that best befits the question asked.
Below is a code excerpt that I use on my frontend Ember App, sitting behind Cloudflare... and varnish... and fastboot...
function parseTrace(url){
let trace = [];
$.ajax(url,
{
success: function(response){
let lines = response.split('\n');
let keyValue;
lines.forEach(function(line){
keyValue = line.split('=');
trace[keyValue[0]] = decodeURIComponent(keyValue[1] || '');
if(keyValue[0] === 'loc' && trace['loc'] !== 'XX'){
alert(trace['loc']);
}
if(keyValue[0] === 'ip'){
alert(trace['ip']);
}
});
return trace;
},
error: function(){
return trace;
}
}
);
};
let cfTrace = parseTrace('/cdn-cgi/trace');
The performance is really really great, don't be afraid to call this function even before you call other APIs or functions. I have found it to be as quick or sometimes even quicker than retrieving static resources from Cloudflare's cache. You can run a profile on Pingdom to confirm this.
Assuming you are talking about client side JavaScript: no, it isn't possible.
The browser makes an HTTP request to the server.
The server notices what IP address the request came from
The server looks up that IP address in a database and finds the matching country
The server passes that country to PHP
The data never even goes near the browser.
For JavaScript to access it, you would need to read it with server side code and then put it in a response back to the browser.
fetch('https://cloudflare-quic.com/b/headers').then(res=>res.json()).then(data=>{console.log(data.headers['Cf-Ipcountry'])})
Reference:
https://cloudflare-quic.com/b
https://cloudflare-quic.com/b/headers
Useful Links:
https://www.cloudflare.com/cdn-cgi/trace
https://github.com/fawazahmed0/cloudflare-trace-api
Yes you have to hit the server - but it doesn't have to be YOUR server.
I have a shopping cart where pretty much everything is cached by Cloudflare - so I felt it would be stupid to go to MY server to get just the countrycode.
Instead I am using a webworker on Cloudflare (additional charges):
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
var countryCode = request.headers.get('CF-IPCountry');
return new Response(
JSON.stringify({ countryCode }),
{ headers: {
"Content-Type": "application/json"
}});
}
You can map this script to a route such as /api/countrycode and then when your client makes an HTTP request it will return essentially instantly (for me it's about 10ms).
/api/countrycode
{
"countryCode": "US"
}
Couple additional things:
You can't use webworkers on all service levels
It would be best to deploy an actual webservice on the same URL as a backup (if webworkers aren't enabled or supported or for during development)
There are charges but they should be neglibible
It seems like there's a new feature where you can map a single path to a single script. That's what I am doing here. I think this used to be an enterprise only feature but it's now available to me so that's great.
Don't forget that it may be T1 for TOR network
Since I wrote this they've exposed more properties on Request.cf - even on lower priced plans:
https://developers.cloudflare.com/workers/runtime-apis/request#incomingrequestcfproperties
You can now get city, region and even longitude and latitude, without having to use a geo lookup database.
I've taken Don Omondi's answer, and converted it to a promise function for ease of use.
function get_country_code() {
return new Promise((resolve, reject) => {
var trace = [];
jQuery.ajax('/cdn-cgi/trace', {
success: function(response) {
var lines = response.split('\n');
var keyValue;
for (var index = 0; index < lines.length; index++) {
const line = lines[index];
keyValue = line.split('=');
trace[keyValue[0]] = decodeURIComponent(keyValue[1] || '');
if (keyValue[0] === 'loc' && trace['loc'] !== 'XX') {
return resolve(trace['loc']);
}
}
},
error: function() {
return reject(trace);
}
});
});
}
usage example
get_country_code().then((country_code) => {
// do something with the variable country_code
}).catch((err) => {
// caught the error, now do something with it
});

Categories