Related
I recently needed to implement an automatic download of a CSV file in javascript. I didn't want to use any 3rd party libraries so instead I studies how the 3rd party libraries do it. I took a look at FileSaver npm package specifically at the function saveAs from here.
Eventually I changed the code to suit my needs into something like this:
class BlobDownload {
constructor(window, blob, fileName, contentType) {
this.window = window;
this.document = window.document;
this.blob = blob;
this.fileName = fileName;
this.contentType = contentType;
}
asyncCreateObjectURL = blob => {
return new Promise((resolve, reject) => {
const blobURL = this.window.URL.createObjectURL(blob);
blobURL ? resolve(blobURL) : reject();
});
}
click = (node) => {
try {
node.dispatchEvent(new MouseEvent('click'));
} catch (e) {
const evt = this.document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, this.window, 0, 0, 0, 80,
20, false, false, false, false, 0, null);
node.dispatchEvent(evt);
}
};
download = async () => {
const blob = new Blob([this.blob], { type: this.contentType });
const a = this.document.createElement('a');
a.download = this.fileName;
a.href = await this.asyncCreateObjectURL(blob);
setTimeout(() => { this.window.URL.revokeObjectURL(a.href) }, 60000) // 1min
setTimeout(() => { this.click(a) }, 0)
}
}
export default BlobDownload;
I don't understand a few things in the code though:
we're create an a node but it doesn't display anywhere on the page. Where does this node actually reside, only as an object in RAM?
the function click dispatched click event immediately, but if this doesn't work it creates a new event and then dispatches it. Why do we need to account for the case where simple dispatching doesn't work?
the whole procedure of creating an temporary a link and artificially clicking on it seems like a hack. Is this really a good pattern or better practices exist to download files?
we're create an a node but it doesn't display anywhere on the page. Where does this node actually reside, only as an object in RAM?
Yes, it will only reside in the memory but it's still possible to call an event on it as evident by the source code. Note: this will not work on iOS Safari. There you have to display the actual link and prompt the user to tap it - there's a note about it on FileSaver.js's README.md.
the function click dispatched click event immediately, but if this doesn't work it creates a new event and then dispatches it. Why do we need to account for the case where simple dispatching doesn't work?
Reading the MDN page for initMouseEvent tells us it deprecated. If the click function fails and the try-catch is triggered then we're on a really old browser where initMouseEvent should be available. If you go back in the commit history the code used to be far more complicated due to edge cases and ultimately not needed feature detection.
the whole procedure of creating an temporary a link and artificially clicking on it seems like a hack. Is this really a good pattern or better practices exist to download files?
It's a hack, there's no way around it. Best practices won't get you far if you need to have backwards compatibility. FileSaver.js strives to abstract away all of the gnarly hacking needed to handle programmatic file saving so that you don't have to.
In general I would advise against copying over library code for a custom implementation. FileSaver.js is over 9 years old now and still relevant. Having it as a package.json dependency or a copy-paste in a library folder would preserve it's attribution making maintenance easier on the next guy :).
In general it's a good set of questions! I'm also implementing a CSV download feature right now. I stumbled on it from googling based on this SO answer while trying to get file saving to work with Web Workers without the need to postMessage the contents of a file. Apparently passing only an URL to a blob is enough for saveAs to work!
// in web worker return from promise-worker/postMessage ->
URL.createObjectURL(new Blob([data], { type: 'text/csv;charset=utf-8;' }));
// on main thread
saveAs(objectURLCreatedAbove);
// done :)
Consider using Web Workers if your CSV's get big and there are considerable freezes when you prepare data for downloading. The sheetjs library is also quite helpful if you need to do anything with spreadsheet data.
Happy coding!
I know that client-side Javascript cannot write data to the local filesystem, for obvious security reasons.
The only way to save data locally with Javascript seems to be with cookies, localStorage, or allow the user to download a file (with a "Save..." dialog box or to the browser's default Download folder).
But is it possible, in the very specific case when the file is accessed locally with an URL like file:///D:/test/index.html (and not through internet) to write data locally ? (without any server language, and even without any server at all: just local browsing of a HTML file)
For example, would it be possible, by clicking on SAVE here:
<div contenteditable="true" style="height:200px;">Content editable - edit me and save!</div>
<button>Save</button>
...that such a HTML file (accessed through file:///D:/test/index.html) is overwritten with its new content ? (i.e. the local HTML file should be updated when SAVE is pressed).
TL;DR: Is this possible to SAVE a file thanks to Javascript, when the HTML page is accessed locally?
Note: I want to be able to silently save, not propose a Download/Save dialog box in which the user has to choose where to download, then "Are you sure to want to overwrite" etc.
EDIT : Why this question? I'm doing an in-browser notepad that I can run locally without any server (no Apache, no PHP). I need to be able to save easily without having to deal with Dialog Box "Where do you want to download the file?" and having to always re-browse to the same folder to overwrite the currently-being-edited file. I would like a simple UX like in any notepad program: CTRL+S done, the current file is saved! (example: MS Word doesn't ask to browse where you want to save the file each time you do "Save": CTRL+S, done!)
You can just use the Blob function:
function save() {
var htmlContent = ["your-content-here"];
var bl = new Blob(htmlContent, {type: "text/html"});
var a = document.createElement("a");
a.href = URL.createObjectURL(bl);
a.download = "your-download-name-here.html";
a.hidden = true;
document.body.appendChild(a);
a.innerHTML = "something random - nobody will see this, it doesn't matter what you put here";
a.click();
}
and your file will save.
The canonical answer, from the W3C File API Standard:
User agents should provide an API exposed to script that exposes the features above. The user is notified by UI anytime interaction with the file system takes place, giving the user full ability to cancel or abort the transaction. The user is notified of any file selections, and can cancel these. No invocations to these APIs occur silently without user intervention.
Basically, because of security settings, any time you download a file, the browser will make sure the user actually wants to save the file. Browsers don't really differentiate JavaScript on your computer and JavaScript from a web server. The only difference is how the browser accesses the file, so storing the page locally will not make a difference.
Workarounds:
However, you could just store the innerHTML of the <div> in a cookie. When the user gets back, you can load it back from the cookie. Although it isn't exactly saving the file to the user's computer, it should have the same effect as overwriting the file. When the user gets back, they will see what they entered the last time. The disadvantage is that, if the user clears their website data, their information will be lost. Since ignoring a user's request to clear local storage is also a security problem, there really is no way around it.
However, you could also do the following:
Use a Java applet
Use some other kind of applet
Create a desktop (non-Web based) application
Just remember to save the file when you clear your website data. You can create an alert that pops up and reminds you, or even opens the save window for you, when you exit the page.
Using cookies: You can use JavaScript cookies on a local page. Just put this in a file and open it in your browser:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<p id="timesVisited"></p>
<script type="text/javascript">
var timesVisited = parseInt(document.cookie.split("=")[1]);
if (isNaN(timesVisited)) timesVisited = 0;
timesVisited++;
document.cookie = "timesVisited=" + timesVisited;
document.getElementById("timesVisited").innerHTML = "You ran this snippet " + timesVisited + " times.";
</script>
</body>
</html>
Chromium's File System Access API (introduced in 2019)
There's a relatively new, non-standard File System Access API (not to be confused with the earlier File and Directory Entries API or the File System API). It looks like it was introduced in 2019/2020 in Chromium/Chrome, and doesn't have support in Firefox or Safari.
When using this API, a locally opened page can open/save other local files and use the files' data in the page. It does require initial permission to save, but while the user is on the page, subsequent saves of specific files do so 'silently'. A user can also grant permission to a specific directory, in which subsequent reads and writes to that directory don't require approval. Approval is needed again after the user closes all the tabs to the web page and reopens the page.
You can read more about this newish API at https://web.dev/file-system-access/. It's meant to be used to make more powerful web applications.
A few things to note about it:
By default, it requires a secure context to run. Running it on https, localhost, or through file:// should work.
You can get a file handle from dragging and dropping a file by using DataTransferItem.getAsFileSystemHandle
Initially reading or saving a file requires user approval and can only be initiated via a user interaction. After that, subsequent reads and saves don't need approval, until the site is opened again.
Handles to files can be saved in the page (so if you were editing 'path/to/file.html', and reload the page, it would be able to have a reference to the file). They can't seemingly be stringified, so are stored through something like IndexedDB (see this answer for more info). Using stored handles to read/write requires user interaction and user approval.
Here are some simple examples. They don't seem to run in a cross-domain iframe, so you probably need to save them as an html file and open them up in Chrome/Chromium.
Opening and Saving, with Drag and Drop (no external libraries):
<body>
<div><button id="open">Open</button><button id="save">Save</button></div>
<textarea id="editor" rows=10 cols=40></textarea>
<script>
let openButton = document.getElementById('open');
let saveButton = document.getElementById('save');
let editor = document.getElementById('editor');
let fileHandle;
async function openFile() {
try {
[fileHandle] = await window.showOpenFilePicker();
await restoreFromFile(fileHandle);
} catch (e) {
// might be user canceled
}
}
async function restoreFromFile() {
let file = await fileHandle.getFile();
let text = await file.text();
editor.value = text;
}
async function saveFile() {
var saveValue = editor.value;
if (!fileHandle) {
try {
fileHandle = await window.showSaveFilePicker();
} catch (e) {
// might be user canceled
}
}
if (!fileHandle || !await verifyPermissions(fileHandle)) {
return;
}
let writableStream = await fileHandle.createWritable();
await writableStream.write(saveValue);
await writableStream.close();
}
async function verifyPermissions(handle) {
if (await handle.queryPermission({ mode: 'readwrite' }) === 'granted') {
return true;
}
if (await handle.requestPermission({ mode: 'readwrite' }) === 'granted') {
return true;
}
return false;
}
document.body.addEventListener('dragover', function (e) {
e.preventDefault();
});
document.body.addEventListener('drop', async function (e) {
e.preventDefault();
for (const item of e.dataTransfer.items) {
if (item.kind === 'file') {
let entry = await item.getAsFileSystemHandle();
if (entry.kind === 'file') {
fileHandle = entry;
restoreFromFile();
} else if (entry.kind === 'directory') {
// handle directory
}
}
}
});
openButton.addEventListener('click', openFile);
saveButton.addEventListener('click', saveFile);
</script>
</body>
Storing and Retrieving a File Handle using idb-keyval:
Storing file handles can be tricky, since they can't be unstringified, though apparently they can be used with IndexedDB and mostly with history.state. For this example we'll use idb-keyval to access IndexedDB to store a file handle. To see it work, open or save a file, and then reload the page and press the 'Restore' button. This example uses some code from https://stackoverflow.com/a/65938910/.
<body>
<script src="https://unpkg.com/idb-keyval#6.1.0/dist/umd.js"></script>
<div><button id="restore" style="display:none">Restore</button><button id="open">Open</button><button id="save">Save</button></div>
<textarea id="editor" rows=10 cols=40></textarea>
<script>
let restoreButton = document.getElementById('restore');
let openButton = document.getElementById('open');
let saveButton = document.getElementById('save');
let editor = document.getElementById('editor');
let fileHandle;
async function openFile() {
try {
[fileHandle] = await window.showOpenFilePicker();
await restoreFromFile(fileHandle);
} catch (e) {
// might be user canceled
}
}
async function restoreFromFile() {
let file = await fileHandle.getFile();
let text = await file.text();
await idbKeyval.set('file', fileHandle);
editor.value = text;
restoreButton.style.display = 'none';
}
async function saveFile() {
var saveValue = editor.value;
if (!fileHandle) {
try {
fileHandle = await window.showSaveFilePicker();
await idbKeyval.set('file', fileHandle);
} catch (e) {
// might be user canceled
}
}
if (!fileHandle || !await verifyPermissions(fileHandle)) {
return;
}
let writableStream = await fileHandle.createWritable();
await writableStream.write(saveValue);
await writableStream.close();
restoreButton.style.display = 'none';
}
async function verifyPermissions(handle) {
if (await handle.queryPermission({ mode: 'readwrite' }) === 'granted') {
return true;
}
if (await handle.requestPermission({ mode: 'readwrite' }) === 'granted') {
return true;
}
return false;
}
async function init() {
var previousFileHandle = await idbKeyval.get('file');
if (previousFileHandle) {
restoreButton.style.display = 'inline-block';
restoreButton.addEventListener('click', async function (e) {
if (await verifyPermissions(previousFileHandle)) {
fileHandle = previousFileHandle;
await restoreFromFile();
}
});
}
document.body.addEventListener('dragover', function (e) {
e.preventDefault();
});
document.body.addEventListener('drop', async function (e) {
e.preventDefault();
for (const item of e.dataTransfer.items) {
console.log(item);
if (item.kind === 'file') {
let entry = await item.getAsFileSystemHandle();
if (entry.kind === 'file') {
fileHandle = entry;
restoreFromFile();
} else if (entry.kind === 'directory') {
// handle directory
}
}
}
});
openButton.addEventListener('click', openFile);
saveButton.addEventListener('click', saveFile);
}
init();
</script>
</body>
Additional Notes
Firefox and Safari support seems to be unlikely, at least in the near term. See https://github.com/mozilla/standards-positions/issues/154 and https://lists.webkit.org/pipermail/webkit-dev/2020-August/031362.html
Yes, it's possible.
In your example, you are already using ContentEditable and most of tutorials for that attribute have some sort of localStrorage example, ie. http://www.html5tuts.co.uk/demos/localstorage/
On page load, script should check localStorage for data and if true, populate element. Any changes in content could be saved in localStorage when clicking save button (or automatically, in linked example, using blur and focus). Additionally you can use this snippet to check weather user is online or offline and based on state modify your logic:
// check if online/offline
// http://www.kirupa.com/html5/check_if_internet_connection_exists_in_javascript.htm
function doesConnectionExist() {
var xhr = new XMLHttpRequest();
var file = "http://www.yoursite.com/somefile.png";
var randomNum = Math.round(Math.random() * 10000);
xhr.open('HEAD', file + "?rand=" + randomNum, false);
try {
xhr.send();
if (xhr.status >= 200 && xhr.status < 304) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
}
EDIT: More advance version of localStorage is Mozilla localForage which allows storing other types of data besides strings.
You could save files, and make it persistent using the FileSystem-API and webkit. You would have to use a chrome browser and it is not a standards technology but I think it does exactly what you want it to do. Here is a great tutorial to show how to do just that http://www.noupe.com/design/html5-filesystem-api-create-files-store-locally-using-javascript-webkit.html
And to show that its on topic it starts off showing you how to make the file save persistent...
window.webkitRequestFileSystem(window.PERSISTENT , 1024*1024, SaveDatFileBro);
Convert your HTML content to a data uri string, and set as href attribute of an anchor element. Don't forget to specify a filename as download attribute.
Here's a simple example:
<a>click to download</a>
<script>
var anchor = document.querySelector('a');
anchor.setAttribute('download', 'example.html');
anchor.setAttribute('href', 'data:text/html;charset=UTF-8,<p>asdf</p>');
</script>
Just try it in your browser, no server required.
Have a look into this :)
Download File Using Javascript/jQuery
there should be everything you need. If you still need help or it's not the solution you need, tell me ;)
Yes, it is possible. Proof by example:
TiddlyFox: allows modification of local files via an add-on. (source code) (extension page):
TiddlyFox is an extension for Mozilla Firefox that enables TiddlyWiki
to save changes directly to the file system.
Todo.html: An HTML file that saves edits to itself. Currently, it only works in Internet Explorer and you have to confirm some security dialogs when first opening the file. (source code) (functional demo).
Steps to confirm todo.html actually saves changes to itself locally:
Save todo.html to local harddrive
Open with Internet Explorer. Accept all the security dialogs.
Type command todo add TEST (todo.html emulates the command-line interface of todo.txt-CLI)
Inspect todo.html file for addition of 'TEST'
Caveats: there is no cross-platform method. I'm not sure how much longer these methods will exist. When I first started my todo.html project, there was a jQuery plugin called twFile that allowed cross-browser loading/saving of local files using four different methods (ActiveX, Mozilla XUL, Java applet, Java Live Connect). Except for ActiveX, browsers have disallowed all these methods due to security concerns.
If you are fine with your code running outside of the scope of your default browser, and you are fine with windows only support, HTAs meet the silently save without prompts requirement easily.
The below code doesn't use many HTA specific features but it does still use microsoft specific stuff like ActiveXObject("Scripting.FileSystemObject").
<html>
<head>
<title>Simple Notepad</title>
<meta http-equiv="X-UA-Compatible" content="IE=9">
<script>
document.addEventListener('keydown', function (event) {
if (event.ctrlKey) {
if (event.key == 's') {
var FSo = new ActiveXObject("Scripting.FileSystemObject");
//see https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/opentextfile-method
var thisFile = FSo.OpenTextFile(window.location.pathname, 2, true, -1);
thisFile.Write(document.getElementsByTagName("html")[0].outerHTML);
thisFile.Close();
// Comment out the below alert to get truly silent saving.
alert('Saved Successfully');
if (event.preventDefault) event.preventDefault();
return false;
}
}
}, false);
</script>
</head>
<body contentEditable="true">
<h1>Press <kbd>CTRL + S</kbd> To Save</h1>
</body>
</html>
It also isn't a very rich editing experience but that can be fixed with some more buttons or keyboard shortcuts I think. Like CTRL + B to embolden selected text. It doesn't have any safety checks as of yet, but binding an event handler to beforeunload should prevent any data loss caused by accidentally closing the program.
HTA's do have other disadvantages too. They don't support ES6 (though transpiling is an option).
Although it is a bit dated, If you're not trying to use modern web features, I think you'll agree that it is very functional and usable.
Edit
I forgot to mention, but HTAs have to be saved with the .hta file extension for mshta.exe to be registered as their file type handler. Which is needed so that you can double click it in windows explorer to open it easily.
See also
Introduction to HTML Applications on MSDN
HTML Applications reference on MSDN
I think it's important to clarify the difference between server and client in this context.
Client/server is a program relationship in which one program (the client) requests a service or resource from another program (the server).
Source: http://searchnetworking.techtarget.com/definition/client-server
I'm not sure you'll find too many advanced applications that don't have at least one server/client relationship. It is somewhat misleading to ask to achieve this without any server, because any time your program speaks to another program, it is a client/server relationship, with the requester being the client and the response coming from the server. This is even if you are working locally. When you want to do something outside of the scope of the browser, you need a hook in a server.
Now, that does not mean you can't achieve this without a server-side specific language. For example, this solution uses NodeJS for the server. WinJS has WinJS.xhr, which uses XmlHttpRequest to serve data to the server.
AJAX seeks to offer the same sort of functions. The point here is that whether you have a program or there is some sort of hook pre-built, when you issue a command like "save file" and the file actually gets saved, there is a program on the other side that is parsing it, whether it's a server-side language or something else, meaning you can't possibly have something like this function without a server to receive the request.
Just use https://github.com/firebase/firepad — See it in action
This doesn’t need a server on your computer, it will reach out and save the data remotely.
Use jsPDF -> https://github.com/MrRio/jsPDF
<div id="content">
<h3>Hello, this is a H3 tag</h3>
<p>a pararaph</p>
</div>
<div id="editor"></div>
<button id="cmd">generate PDF</button>
Javascript
var doc = new jsPDF();
var specialElementHandlers = {
'#editor': function (element, renderer) {
return true;
}
};
$('#cmd').click(function () {
doc.fromHTML($('#content').html(), 15, 15, {
'width': 170,
'elementHandlers': specialElementHandlers
});
doc.save('sample-file.pdf');
});
This is an example for those who want to know how to use the localStorage.
<div id="divInput" contenteditable="true" style="height:200px;border: 2px solid blue">
Content editable - edit me and save!
</div>
<button onclick="onSave()">Save</button>
<button onclick="onLoad()">Load</button>
<script>
config = {
localStorageItemName: "demo",
datetimeFormat: {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
hour12: false,
minute: '2-digit',
second: '2-digit'
}
}
function Now() {
return new Date().toLocaleString("zh-TW", config.datetimeFormat)
}
const errMap = {
IsEmptyError: new Error('is empty'),
LengthError: new Error('length = 0')
}
/**
* #param {string} itemName
* #return {Object}
* */
function getLocalStorageItem(itemName) {
const dbDataString = localStorage.getItem(itemName)
if (dbDataString === null) {
throw errMap.IsEmptyError
}
const db = JSON.parse(dbDataString)
if (Object.keys(db).length === 0) {
throw errMap.LengthError
}
return db
}
function onSave() {
const inputValue = document.querySelector(`#divInput`).textContent
try {
const db = getLocalStorageItem(config.localStorageItemName)
db.msg = inputValue
db.lastModTime = Now()
localStorage.setItem(config.localStorageItemName, JSON.stringify(db))
console.log("save OK!")
} catch (err) {
switch (err) {
case errMap.IsEmptyError:
console.info("new localStorageItemName")
localStorage.setItem(config.localStorageItemName,
JSON.stringify({msg: inputValue, createTime: Now()})
)
break
/*
case ...
break
*/
default:
console.error(err.message)
}
}
}
function onLoad(e) {
try {
const db = getLocalStorageItem(config.localStorageItemName)
console.log("load")
document.querySelector(`#divInput`).textContent = db.msg
} catch (err) {
return
}
}
(()=>{
window.onload = () => (
onLoad()
)
})()
</script>
It is written in pure javascript with no dependencies.
I need some help on how I could check the internet connection using Javascript or jQuery or any library if available. cause i'm developing an offline application and I want to show a version if the user is offline and another version if the user is online.
For the moment i'm using this code :
if (navigator.onLine) {
alert('online');
} else {
alert('offline');
}
But this is working very slow to detect. sometimes it's just connected to a network without internet, it takes 5 to 10 seconds to alert false (No internet).
I took a look at Offline.js library, but I'm not sure if this library is useful in my case. and I don't know how to use it
I just got this bit of code functionality from a Mozilla Site:
window.addEventListener('load', function(e) {
if (navigator.onLine) {
console.log('We\'re online!');
} else {
console.log('We\'re offline...');
}
}, false);
window.addEventListener('online', function(e) {
console.log('And we\'re back :).');
}, false);
window.addEventListener('offline', function(e) {
console.log('Connection is down.');
}, false);
They even have a link to see it working. I tried it in IE, Firefox and Chrome. Chrome appeared the slowest but it was only about half a second.
i think you should try OFFLINE.js.. it looks pretty easy to use, just give it a try.
it even provides the option checkOnLoad which checks the connection immediately on page load.
Offline.check(): Check the current status of the connection.
Offline.state: The current state of the connection 'up' or 'down'
haven't tried it, would be nice to know if it works as intended.
EDIT took a little peak into the code, it uses the method with FAILED XHR REQUEST suggested in THIS SO Question
Take a look at Detect that the Internet connection is offline? Basically, make an ajax request to something you know is likely to be up (say google.com) and if it fails, there is no internet connection.
navigator.onLine is a property that maintains a true/false value (true for online, false for offline). This property is updated whenever the user switches into "Offline Mode".
window.addEventListener('load', function() {
function updateOnlineStatus(event) {
document.body.setAttribute("data-online", navigator.onLine);
}
updateOnlineStatus();
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
});
// check if online/offline
// http://www.kirupa.com/html5/check_if_internet_connection_exists_in_javascript.htm
function doesConnectionExist() {
var xhr = new XMLHttpRequest();
var file = "http://www.yoursite.com/somefile.png";
var randomNum = Math.round(Math.random() * 10000);
xhr.open('HEAD', file + "?rand=" + randomNum, false);
try {
xhr.send();
if (xhr.status >= 200 && xhr.status < 304) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
}
My solution is to grab a very small image (1x1), not cached and always onLine.
<head>
<script src="jquery.min.js"></script>
</head>
<body>
<script>
$( document ).ready(function() {
function onLine() {
alert("onLine")
}
function offLine() {
alert("offLine")
}
var i = new Image();
i.onload = onLine;
i.onerror = offLine;
i.src = 'http://www.google-analytics.com/__utm.gif';
});
</script>
<body>
Notes:
Use a local copy of jQuery otherwise it won't work offLine.
I've tested the code onLine/offLine and it works without delay.
Works with all browsers, Desktop or Mobile.
In case you wonder, there's no tracking made from Google Analytics as we don't use any arguments.
Feel free to change the image, just make sure it doesn't get cached and it's small in size.
Try utilizing WebRTC , see diafygi/webrtc-ips; in part
Additionally, these STUN requests are made outside of the normal
XMLHttpRequest procedure, so they are not visible in the developer
console or able to be blocked by plugins such as AdBlockPlus or
Ghostery. This makes these types of requests available for online
tracking if an advertiser sets up a STUN server with a wildcard
domain.
modified minimally to log "online" or "offline" at console
// https://github.com/diafygi/webrtc-ips
function online(callback){
//compatibility for firefox and chrome
var RTCPeerConnection = window.RTCPeerConnection
|| window.mozRTCPeerConnection
|| window.webkitRTCPeerConnection;
var useWebKit = !!window.webkitRTCPeerConnection;
//bypass naive webrtc blocking using an iframe
if(!RTCPeerConnection) {
//NOTE: you need to have an iframe in the page
// right above the script tag
//
//<iframe id="iframe" sandbox="allow-same-origin" style="display: none"></iframe>
//<script>...getIPs called in here...
//
var win = iframe.contentWindow;
RTCPeerConnection = win.RTCPeerConnection
|| win.mozRTCPeerConnection
|| win.webkitRTCPeerConnection;
useWebKit = !!win.webkitRTCPeerConnection;
}
//minimal requirements for data connection
var mediaConstraints = {
optional: [{RtpDataChannels: true}]
};
//firefox already has a default stun server in about:config
// media.peerconnection.default_iceservers =
// [{"url": "stun:stun.services.mozilla.com"}]
var servers = undefined;
//add same stun server for chrome
if(useWebKit)
servers = {iceServers: [{urls: "stun:stun.services.mozilla.com"}]};
//construct a new RTCPeerConnection
var pc = new RTCPeerConnection(servers, mediaConstraints);
//create a bogus data channel
pc.createDataChannel("");
var fn = function() {};
//create an offer sdp
pc.createOffer(function(result){
//trigger the stun server request
pc.setLocalDescription(result, fn, fn);
}, fn);
//wait for a while to let everything done
setTimeout(function(){
//read candidate info from local description
var lines = pc.localDescription.sdp.split("\n");
// return `true`:"online" , or `false`:"offline"
var res = lines.some(function(line) {
return line.indexOf("a=candidate") === 0
});
callback(res);
}, 500);
}
//Test: Print "online" or "offline" into the console
online(function(connection) {
if (connection) {
console.log("online")
} else {
console.log("offline")
}
});
You can use SignalR, if you're developing using MS web technologies. SignalR will establish either long polling or web sockets depending on your server/client browser technology, transparent to you the developer. You don't need to use it for anything else than determining if you have an active connection to the site or not.
If SignalR disconnects for any reason, then you have lost connection to the site, as long as your SignalR server instance is actually installed on the site. Thus, you can use $.connection.hub.disconnected() event/method on the client to set a global var which holds your connection status.
Read up about SignalR and how to use it for determining connection states here...
http://www.asp.net/signalr/overview/guide-to-the-api/handling-connection-lifetime-events#clientdisconnect
See How do I check connection type (WiFi/LAN/WWAN) using HTML5/JavaScript? answers:
Rob W suggests navigator.connection;
Bergi suggests Windows.Networking.Connectivity API through this tutorial;
Gerard Sexton suggests Gmail approach.
You can use the new Fetch API which will trigger an error almost immediately if no network is present.
The problem with this is that the Fetch API has infant support at the moment (currently Chrome has the most stable implementation, Firefox and Opera is getting there, IE does not support it). There exists a polyfill to support the fetch principle but not necessarily the rapid return as with a pure implementation. On the other hand, an offline app would require a modern browser...
An example which will try to load a plain text file over HTTPS to avoid CORS requirements (link is picked at random, you should set up a server with a tiny text file to test against - test in Chrome, for now):
fetch("https://link.to/some/testfile")
.then(function(response) {
if (response.status !== 200) { // add more checks here, ie. 30x etc.
alert("Not available"); // could be server errors
}
else
alert("OK");
})
.catch(function(err) {
alert("No network"); // likely network errors (incl. no connection)
});
Another option is to set up a Service worker and use fetch from there. This way you could serve an optional/custom offline page or a cached page when the requested page is not available. Also this is a very fresh API.
best one liner
console.log(navigator.onLine ? 'online' : 'offline');
I am developing a library which I want to host on a CDN. The library is going to be used on many different domains across multiple servers. The library itself contains one script (let's call it script.js for now) which loads a web worker (worker.js).
Loading the library itself is quite easy: just add the <script type="text/javascript" src="http://cdn.mydomain.com/script.js"></script> tag to the domain on which I want to use the library (www.myotherdomain.com). However since the library is loading a worker from http://cdn.mydomain.com/worker.js new Worker('http://cdn.mydomain.com/worker.js'), I get a SecurityException. CORS is enabled on cdn.mydomain.com.
For web workers it is not allowed to use a web worker on a remote domain. Using CORS will not help: browsers seem to ignore it and don't even execute the preflight check.
A way around this would be to perform an XMLHttpRequest to get the source of the worker and then create a BLOB url and create a worker using this url. This works for Firefox and Chrome. However, this does not seem to work for Internet Explorer or Opera.
A solution would be to place the worker on www.myotherdomain.com or place a proxy file (which simply loads the worker from the cdn using XHR or importScripts). I do not however like this solution: it requires me to place additional files on the server and since the library is used on multiple servers, updating would be difficult.
My question consists of two parsts:
Is it possible to have a worker on a remote origin for IE 10+?
If 1 is the case, how is it handled best to be working cross-browser?
The best is probably to generate a simple worker-script dynamically, which will internally call importScripts(), which is not limited by this cross-origin restriction.
To understand why you can't use a cross-domain script as a Worker init-script, see this answer. Basically, the Worker context will have its own origin set to the one of that script.
// The script there simply posts back an "Hello" message
// Obviously cross-origin here
const cross_origin_script_url = "https://greggman.github.io/doodles/test/ping-worker.js";
const worker_url = getWorkerURL( cross_origin_script_url );
const worker = new Worker( worker_url );
worker.onmessage = (evt) => console.log( evt.data );
URL.revokeObjectURL( worker_url );
// Returns a blob:// URL which points
// to a javascript file which will call
// importScripts with the given URL
function getWorkerURL( url ) {
const content = `importScripts( "${ url }" );`;
return URL.createObjectURL( new Blob( [ content ], { type: "text/javascript" } ) );
}
For those who find this question:
YES.
It is absolutely possible: the trick is leveraging an iframe on the remote domain and communicating with it through postMessage. The remote iframe (hosted on cdn.mydomain.com) will be able to load the webworker (located at cdn.mydomain.com/worker.js) since they both have the same origin.
The iframe can then act as a proxy between the postMessage calls. The script.js will however be responsible from filtering the messages so only valid worker messages are handled.
The downside is that communication speeds (and data transfer speeds) do take a performance hit.
In short:
script.js appends iframe with src="//cdn.mydomain.com/iframe.html"
iframe.html on cdn.mydomain.com/iframe.html, executes new Worker("worker.js") and acts as a proxy for message events from window and worker.postMessage (and the other way around).
script.js communicates with the worker using iframe.contentWindow.postMessage and the message event from window. (with the proper checks for the correct origin and worker identification for multiple workers)
It's not possible to load a web worker from a different domain.
Similar to your suggestion, you could make a fetch call, then take that JS and base64 it. Doing so allows you to do:
const worker = new Worker(`data:text/javascript;base64,${btoa(workerJs)}`)
You can find out more info about data URIs here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs.
This is the workaround I prefer because it doesn't require anything crazy like an iframe with a message proxy and is very simple to get working provided you setup CORS correctly from your CDN.
Since #KevinGhadyani answer (or blob techniques) require to lessen your CSPs (by adding a worker-src data: or blob: directive, for example), there is a little example of how you can take advantage of importScripts inside a worker to load another worker script hosted on another domain, without lessening your CSPs.
It may help you to load a worker from any CDN allowed by your CSPs.
As far as I know, it works on Opera, Firefox, Chrome, Edge and all browsers that support workers.
/**
* This worker allow us to import a script from our CDN as a worker
* avoiding to have to reduce security policy.
*/
/**
* Send a formated response to the main thread. Can handle regular errors.
* #param {('imported'|'error')} resp
* #param {*} data
*/
function respond(resp, data = undefined){
const msg = { resp };
if(data !== undefined){
if(data && typeof data === 'object'){
msg.data = {};
if(data instanceof Error){
msg.error = true;
msg.data.code = data.code;
msg.data.name = data.name;
msg.data.stack = data.stack.toString();
msg.data.message = data.message;
} else {
Object.assign(msg.data, data);
}
} else msg.data = data;
}
self.postMessage(msg);
}
function handleMessage(event){
if(typeof event.data === 'string' && event.data.match(/^#worker-importer/)){
const [
action = null,
data = null
] = event.data.replace('#worker-importer.','').split('|');
switch(action){
case 'import' :
if(data){
try{
importScripts(data);
respond('imported', { url : data });
//The work is done, we can just unregister the handler
//and let the imported worker do it's work without us.
self.removeEventListener('message', handleMessage);
}catch(e){
respond('error', e);
}
} else respond('error', new Error(`No url specified.`));
break;
default : respond('error', new Error(`Unknown action ${action}`));
}
}
}
self.addEventListener('message', handleMessage);
How to use it ?
Obviously, your CSPs must allow the CDN domain, but you don't need more CSP rule.
Let's say that you domain is my-domain.com, and your cdn is statics.your-cdn.com.
The worker we want to import is hosted at https://statics.your-cdn.com/super-worker.js and will contain :
self.addEventListener('message', event => {
if(event.data === 'who are you ?') {
self.postMessage("It's me ! I'm useless, but I'm alive !");
} else self.postMessage("I don't understand.");
});
Assuming that you host a file with the code of the worker importer on your domain (NOT your CDN) under the path https://my-domain.com/worker-importer.js, and that you try to start your worker inside a script tag at https://my-domain.com/, this is how it works :
<script>
window.addEventListener('load', async () => {
function importWorker(url){
return new Promise((resolve, reject) => {
//The worker importer
const workerImporter = new Worker('/worker-importer.js');
//Will only be used to import our worker
function handleImporterMessage(event){
const { resp = null, data = null } = event.data;
if(resp === 'imported') {
console.log(`Worker at ${data.url} successfully imported !`);
workerImporter.removeEventListener('message', handleImporterMessage);
// Now, we can work with our worker. It's ready !
resolve(workerImporter);
} else if(resp === 'error'){
reject(data);
}
}
workerImporter.addEventListener('message', handleImporterMessage);
workerImporter.postMessage(`#worker-importer.import|${url}`);
});
}
const worker = await importWorker("https://statics.your-cdn.com/super-worker.js");
worker.addEventListener('message', event => {
console.log('worker message : ', event.data);
});
worker.postMessage('who are you ?');
});
</script>
This will print :
Worker at https://statics.your-cdn.com/super-worker.js successfully imported !
worker message : It's me ! I'm useless, but I'm alive !
Note that the code above can even work if it's written in a file hosted on the CDN too.
This is especially usefull when you have several worker scripts on your CDN, or if you build a library that must be hosted on a CDN and you want your users to be able to call your workers without having to host all workers on their domain.
I'm writing a Firefox extension that creates a socket server which will output the active tab's URL when a client makes a connection to it. I have the following code in my javascript file:
var serverSocket;
function startServer()
{
var listener =
{
onSocketAccepted : function(socket, transport)
{
try {
var outputString = gBrowser.currentURI.spec + "\n";
var stream = transport.openOutputStream(0,0,0);
stream.write(outputString,outputString.length);
stream.close();
} catch(ex2){ dump("::"+ex2); }
},
onStopListening : function(socket, status){}
};
try {
serverSocket = Components.classes["#mozilla.org/network/server-socket;1"]
.createInstance(Components.interfaces.nsIServerSocket);
serverSocket.init(7055,true,-1);
serverSocket.asyncListen(listener);
} catch(ex){ dump(ex); }
document.getElementById("status").value = "Started";
}
function stopServer ()
{
if (serverSocket)
serverSocket.close();
}
window.addEventListener("load", function() { startServer(); }, false);
window.addEventListener("unload", function() { stopServer(); }, false);
As it is, it works for multiple tabs in a single window. If I open multiple windows, it ignores the additional windows. I think it is creating a server socket for each window, but since they are using the same port, the additional sockets fail to initialize. I need it to create a server socket when the browser launches and continue running when I close the windows (Mac OS X). As it is, when I close a window but Firefox remains running, the socket closes and I have to restart firefox to get it up an running. How do I go about that?
Firefox extension overlays bind to window objects. One way around this is to create an XPCOM component or find one that someone else already created to allow you to build functionality without binding it to the window objects.
Of course, section #2 below on Observer Notifications may be helpful as well.
Possible workaround: #1
Instead of calling "startServer()" each time a window is opened, you could have a flag called windowCount that you could increment each time you open a new window. If windowCount is greater than 0, don't call startServer().
As windows close, you could decrement the count. Once it hits 0, stop the server.
Here is information from the Mozilla forums on this problem:
http://forums.mozillazine.org/viewtopic.php?f=19&t=2030279
Possible workaround #2:
With that said, I've also found documentation for Observer Notifications, which may be helpful as there is a section on Application Startup and Shutdown:
https://developer.mozilla.org/en/Observer_Notifications
UPDATE:
Here are some resources on creating XPCOM components in JavaScript and in C++:
https://developer.mozilla.org/en/how_to_build_an_xpcom_component_in_javascript
http://www.codeproject.com/KB/miscctrl/XPCOM_Creation.aspx
https://developer.mozilla.org/en/creating_xpcom_components
You probably want to:
Move your code into a JavaScript component
Register your component as a profile-after-change observer
Whenever someone makes a connection to your socket, find the active window and return its URL.
Use something like
var wm = Components.classes["#mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow("navigator:browser");
var spec = win ? win.getBrowser().currentURI.spec : "";
var outputString = spec + "\n";
etc.