URL of page currently being served by Service Worker 'fetch' event - javascript

How can I get the full URL of the page which is being serviced by a service worker's 'fetch' event?
The "self.location" property seems to only refer to the root URL of the site. For example, if page https://example.com/folder/pagename.html is performing a fetch which the service worker is intercepting, the service worker's 'self.location' property returns "https://example.com".
event.currentTarget.location and event.explicitOriginalTarget.location, event.originalTarget, and event.target all return the URL of the service worker .js file.
How can I get the full URL of the page that triggered the fetch event?

You've got two general approaches, depending on how involved you want to get:
Use the 'Referer' header info
If the request is for a subresource and includes a Referer header, then there's a decent chance that the value of that header is the URL of the page that made the request. (There are some caveats; read this background info to delve into that.)
From within a fetch handler, you can read the value of that header with the following:
self.addEventListener('fetch', event => {
const clientUrl = event.request.referrer;
if (clientUrl) {
// Do something...
}
});
Use the clientId value
Another approach is to use the clientId value that (might) be exposed on the FetchEvent, and then use clients.get(id) or loop through the output of clients.matchAll() to find the matching WindowClient. You could then read the url property of that WindowClient.
One caveat with this approach is that the methods which look up the WindowClient are all asynchronous, and return promises, so if you're somehow using the URL of the client window to determine whether or not you want to call event.respondWith(), you're out of luck (that decision needs to be made synchronously, when the FetchEvent handler is first invoked).
There's a combination of different things that need to be supported in order for this approach to work, and I'm not sure which browsers currently support everything I mentioned. I know Chrome 67 does, for instance (because I just tested it there), but you should check in other browsers if this functionality is important to you.
self.addEventListener('fetch', async event => {
const clientId = event.clientId;
if (clientId) {
if ('get' in clients) {
const client = await clients.get(clientId);
const clientUrl = client.url;
// Do something...
} else {
const allClients = await clients.matchAll({type: 'window'});
const filtered = allClients.filter(client => client.id === clientId);
if (filtered.length > 0) {
const clientUrl = filtered[0].url;
// Do something...
}
}
}
});

Related

Why does my Firestore listener with offline persistence always read from the server?

I am using Firebase JavaScript Modular Web Version 9 SDK with my Vue 3 / TypeScript app.
My understanding is that when using Firestore real-time listeners with offline persistence it should work like this:
When the listener is started the callback fires with data read from the local cache, and then immediately after it also tries to read from the server to make sure the local cache has up to date values. If the server data matches the local cache the callback listener should only fire once with data read from the local cache.
When data changes, the callback listener fires with data read from the server. It uses that data to update the local cache.
When data doesn't change, all subsequent calls to the listener trigger a callback with data read from the local cache.
But I have setup offline persistence, created a listener for my Firestore data, and monitored where the reads were coming from...
And in my app I see an initial read from the local cache (expected), and then a second immediate read from the server (unexpected). And after that all subsequent reads are coming from the server (also unexpected).
During this testing none of my data has changed. So I expected all reads from the callback listener to be coming from the local cache, not the server.
And actually the only time I see a read from the local cache is when the listener is first started, but this was to be expected.
What could be the problem?
P.S. To make those "subsequent calls" I am navigating to a different page of my SPA and then coming back to the page where my component lives to trigger it again.
src/composables/database.ts
export const useLoadWebsite = () => {
const q = query(
collection(db, 'websites'),
where('userId', '==', 'NoLTI3rDlrZtzWCbsZpPVtPgzOE3')
);
const firestoreWebsite = ref<DocumentData>();
onSnapshot(q, { includeMetadataChanges: true }, (querySnapshot) => {
const source = querySnapshot.metadata.fromCache ? 'local cache' : 'server';
console.log('Data came from ' + source);
const colArray: DocumentData[] = [];
querySnapshot.docs.forEach((doc) => {
colArray.push({ ...doc.data(), id: doc.id });
});
firestoreWebsite.value = colArray[0];
});
return firestoreWebsite;
};
src/components/websiteUrl.vue
<template>
<div v-if="website?.url">{{ website.url }}</div>
</template>
<script setup lang="ts">
import { useLoadWebsite } from '../composables/database';
const website = useLoadWebsite();
</script>
Nothing is wrong. What you're describing is working exactly the way I would expect.
Firestore local persistence is not meant to be a full replacement for the backend. By default, It's meant to be a temporary data source in the case that the backend is not available. If the backend is available, then the SDK will prefer to ensure that the client app is fully synchronized with it, and serve all updates from that backend as long as it's available.
If you want to force a query to use only the cache and not the backend, you can programmatically specify the cache as the source for that query.
If you don't want any updates at all from the server for whatever reason, then you can disable network access entirely.
See also:
Firestore clients: To cache, or not to cache? (or both?)
I figured out why I was getting a result different than expected.
The culprit was { includeMetadataChanges: true }.
As explained here in the docs, that option will trigger a listener event for metadata changes.
So the listener callback was also triggering on each metadata change, instead of just data reads and writes, causing me to see strange results.
After removing that it started to work as expected, and I verified it by checking it against the Usage graphs in Firebase console which show the number of reads and snapshot listeners.
Here is the full code with that option removed:
export const useLoadWebsite = () => {
const q = query(
collection(db, 'websites'),
where('userId', '==', 'NoLTI3rDlrZtzWCbsZpPVtPgzOE3')
);
const firestoreWebsite = ref<DocumentData>();
onSnapshot(q, (querySnapshot) => {
const source = querySnapshot.metadata.fromCache ? 'local cache' : 'server';
console.log('Data came from ' + source);
const colArray: DocumentData[] = [];
querySnapshot.docs.forEach((doc) => {
colArray.push({ ...doc.data(), id: doc.id });
});
firestoreWebsite.value = colArray[0];
});
return firestoreWebsite;
};

Is it possible to get the value of window.location.pathname from within a Service Worker?

I am trying to get the value from window.location.pathname (or a similar location read only API) inside the context of a ServiceWorker. I think one way to do that is sending that information from the page to the Service Worker via postMessage:
navigator.serviceWorker.ready.then( registration => {
registration.active.postMessage({
type: "pathname",
value: window.location.pathname
});
});
as seen in https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/message_event
However, I need that data in the install step of the SW lifecycle so waiting on the SW to become the active one is not ideal, and I'd rather try first to get that data earlier so I can go thru the install step with that information.
Within the Service Worker, self.location is accessible via WorkerGlobalScope.location. You could listen to requests and process those that match the same origin of your domain.
self.addEventListener('fetch', event => {
const requestUrl = new URL(event.request.url)
if (self.location.origin === requestUrl.origin) {
const requestPathname = requestUrl.pathname
}
})

Is it possible to link a random html site with node javascript?

Is it possible to link a random site with node.js, when I say that, Is it possible to link it with only a URL, if not then I'm guessing it's having the file.html inside the javascript directory. I really wanna know if it's possible because the html is not mine and I can't add the line of code to link it with js that goes something like (not 100% sure) <src = file.html>
I tried doing document = require('./page.html'); and ('./page') but it didn't work and when I removed the .html at the end of require it would say module not found
My keypoint is that the site shows player count on some servers, and I wanna get that number by linking it with js and then using it in some code which I have the code to (tested in inspect element console) but I don't know how to link it properly to JS.
If you wanna take a look at the site here it is: https://portal.srbultras.info/#servers
If you have any ideas how to link a stranger's html with js, i'd really appreciate to hear it!
You cannot require HTML files unless you use something like Webpack with html-loader, but even in this case you can only require local files. What you can do, however, is to send an HTTP Request to the website. This way you get the same HTML your browser receives whenever you open a webpage. After that you will have to parse the HTML in order to get the data you need. The jsdom package can be used for both steps:
const { JSDOM } = require('jsdom');
JSDOM.fromURL('https://portal.srbultras.info/')
.then(({ window: { document }}) => {
const servers = Array.from(
document.querySelectorAll('#servers tbody>tr')
).map(({ children }) => {
const name = children[3].textContent;
const [ip, port] = children[4]
.firstElementChild
.textContent
.split(':');
const [playersnum, maxplayers] = children[5]
.lastChild
.textContent
.split('/')
.map(n => Number.parseInt(n));
return { name, ip, port, playersnum, maxplayers };
});
console.log(servers);
/* Your code here */
});
However, grabbing the server information from a random website is not really what you want to do, because there is a way to get it directly from the servers. Counter Strike 1.6 servers seem to use the GoldSrc / Source Server Protocol that lets us retrieve information about the servers. You can read more about the protocol here, but we are just going to use the source-server-query package to send queries:
const query = require('source-server-query');
const servers = [
{ ip: '51.195.60.135', port: 27015 },
{ ip: '51.195.60.135', port: 27017 },
{ ip: '185.119.89.86', port: 27021 },
{ ip: '178.32.137.193', port: 27500 },
{ ip: '51.195.60.135', port: 27018 },
{ ip: '51.195.60.135', port: 27016 }
];
const timeout = 5000;
Promise.all(servers.map(server => {
return query
.info(server.ip, server.port, timeout)
.then(info => Object.assign(server, info))
.catch(console.error);
})).then(() => {
query.destroy();
console.log(servers);
/* Your code here */
});
Update
servers is just a normal JavaScript array consisting of objects that describe servers, and you can see its structure when it is logged into the console after the information has been received, so it should not be hard to work with. For example, you can access the playersnum property of the third server in the list by writing servers[2].playersnum. Or you can loop through all the servers and do something with each of them by using functions like map and forEach, or just a normal for loop.
But note that in order to use the data you get from the servers, you have to put your code in the callback function passed to the then method of Promise.all(...), i.e. where console.log(servers) is located. This has to do with the fact that it takes some time to get the responses from the servers, and for that reason server queries are normally asynchronous, meaning that the script continues execution even though it has not received the responses yet. So if you try to access the information in the global scope instead of the callback function, it is not going to be there just yet. You should read about JavaScript Promises if you want to understand how this works.
Another thing you may want to do is to filter out the servers that did not respond to the query. This can happen if a server is offline, for example. In the solution I have provided, such servers are still in the servers array, but they only have the ip and port properties they had originally. You could use filter in order to get rid of them. Do you see how? Tell me if you still need help.

Maintaining state within a conversation within DialogFlow

I am trying to understand how I can manage some aspect of state within fullfillment (DialogFlow's implementation where you can write code in JavaScript and it executes within a Google Cloud Function). First, I would assume that this implementation is stateless, but there must be a way to maintain some state without having to store the data in the database and then retrieve it on the next execution.
I would simply like to maintain the full history of the chat - the question asked by the user, and the response from the chatbot. I can see that I can get this information on every response (and call to the fullfillment) via:
console.log(JSON.stringify(request.body.queryResult.queryText));
console.log(JSON.stringify(request.body.queryResult.fulfillmentText));
Now that I have this information I just want to append it to some variable that is statefull. I have looked into setContext, context.set, app.data, and other functions/variables, but I can't seem to get it working because I'm not sure I understand how it should work.
In my code I have mostly the basic template. I don't think I can use a global variable so how can I store this state (fullConversation) between intent executions just for this user's conversation?
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
let query = JSON.stringify(request.body.queryResult.queryText);
let response = console.log(JSON.stringify(request.body.queryResult.fulfillmentText);
// here I want to retrieve the prior query/response and append it
// i.e., let fullConversation = fullConversation + query + response
}
function welcome(agent) {
agent.add(`Welcome to my agent!`);
}
function fallback(agent) {
agent.add(`I didn't understand`);
agent.add(`I'm sorry, can you try again?`);
}
function myNewHandler(agent) {
}
// Run the proper function handler based on the matched Dialogflow intent name
let intentMap = new Map();
intentMap.set('Default Welcome Intent', welcome);
intentMap.set('Default Fallback Intent', fallback);
intentMap.set('myIntent',myNewHandler);
agent.handleRequest(intentMap);
});
UPDATE:
If I update my code with the code suggestion from #Prisoner I'm still having issues with just getting the context. I never get to my console.log(2). Do I need to move the agent.context.get code outside of the onRequest block??:
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
console.log(JSON.stringify(request.body.queryResult.queryText));
console.log(JSON.stringify(request.body.queryResult.fulfillmentText));
console.log("1");
// Get what was set before, or an empty array
const historyContext = agent.context.get('history');
console.log("2");
SECOND UPDATE:
The issue is related to a known issue solved here.
Just needed to update the dialogflow-fulfillment in package.json and everything worked.
You're on the right track. Global variables are definitely not the way to do it. And state can be maintained as part of a Context.
The app.data property is only available if you're using the actions-on-google library, which it does not look like you're using. Several of the APIs have also changed over time, and can be confusing. See this older answer for an examination of some of the options.
Since you're using the dialogflow-fulfillment library, you'll be using the agent.context (note the singular) object to add new contexts. For the context, you'll want to set a context parameter with the value that you want to store. Values need to be strings - so if you have something like an array, you probably want to convert it to a string using something like JSON.serialzie() and extract it with JSON.parse().
The code that gets the current context with your stored information, and then updates it with the latest values, might look something like this:
// Get what was set before, or an empty array
const historyContext = agent.context.get('history');
const historyString = (historyContext && historyContext.params && historyContext.params.history) || '[]';
const history = JSON.parse(historyString);
// Add the messages as a single object
history.push({
requestMessage,
responseMessage
});
// Save this as a context with a long lifespan
agent.context.set('history', 99, JSON.stringify(history));
Update
I would put this code in a function, and call this function before you return from each handler function you're in. I'm a little surprised that agent.context would be causing problems outside the handler - but since you don't seem to have any specific error, that's my best guess.

Firefox - Intercept/Modify post data when some variable match some pattern

I would like to know if it is possible to intercept and modify a post data when the url and some of the variables meet some pattern.
for example:
let the login url be: http://www.someonlineprofiles.com
let the post data be:
email: "myemail#gmail.com"
pass: "mypass"
theme: "skyblue"
I would like that if:
url = "http://www.someonlineprofiles.com/ajax/login_action_url" and
email = "myemail#gmail.com"
then theme value be unconditionally changed to: "hotdesert"
Is it possible to create a Firefox add-on for that?, are add-ons powerful enough for that?
I found this link:
modify the post data of a request in firefox extension
Thanks in advance!
[ADDED INFORMATION]
I don't know if it is interesting to know the version of my Firefox: 35.0.1
Your question borders on being too broad, so I will give only an overview on how to do this, but not a copy-paste-ready solution, which would take a while to create, and would also deny you a learning experience.
Observers
First of all, it is possible for add-ons to observe and manipulate HTTP(S) requests before the browser sends the request, you just need to implement and register what is called a http observer.
const {classes: Cc, instances: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm"); // for Services
var httpRequestObserver = {
observe: function(channel, topic, data) {
if (topic != "http-on-modify-request") {
return;
}
if (!(channel instanceof Ci.nsIHttpChannel)) {
return; // Not actually a http channel
}
// See nsIChannel, nsIHttpChannel and nsIURI/nsIURL
if (channel.URI.host != "www.someonlineprofiles.com") {
return;
}
doSomething(channel);
},
register: function() {
Services.obs.addObserver(this, "http-on-modify-request", false);
},
unregister: function() {
Services.obs.removeObserver(this, "http-on-modify-request");
}
};
httpObserver.register();
// When your add-on is shut down, don't forget to call httpObserver.unregister();
Do only register the http observer once in your add-on:
If you're using the SDK, then put it into main.js or a dedicated module. You'll also need to rewrite the code a bit and replace the const .. = Components line with a require("chrome").
If you're writing a XUL overlay add-on, put it into a code module.
Rewriting post data
We still need to implement doSomething() and actually rewrite the post data. An http channel usually implements the nsIUploadStream interface, and the upload stream is where the current post data is, if any. It also has a setUploadStream() method, which you can use to replace the upload stream entirely.
function doSomething(channel) {
if (!(channel instanceof Ci.nsIUploadStream)) {
return;
}
// construct new post data
channel.setUploadStream(newStream);
}
Constructing the new post data would be a matter of your actual requirements. I provided a working example in another answer on how you could do it.
If you need to fetch some data from the old upload stream, you'll need to decode the existing channel.uploadStream as multipart/form-data yourself. I suggest you check TamperData and similar add-ons on how they do things there.

Categories