I'd like to create an email from a Javascript web application. I'm completely aware of the many SO questions on this (e.g. Open Outlook HTML with Chrome). There are problems with the typical answers:
Mailto: link: This will let you create an email, but only in plain text (no HTML) and it does not allow for attachments.
Activex: IE only, my application needs to run in Firefox and Chrome too. FF & Chrome plug-ins to allow ActiveX are security hazards and seem to be buggy.
Server-side sends via SMTP: The email does not end up in the "Sent" folder for the user. Plus hurdles letting the user edit HTML in the browser and attach files.
Create an Outlook .MSG file: There seem to be no libraries and little written about doing this. Apparently the file format actually has an entire FAT file storage system embedded.
Key differences between many other SO questions and mine:
I do have access to the client machines, so I could install
helper applications or add-ins, change settings as needed, etc.
The interface does not need to actually send the mail, it only needs
to set it up for the user.
I also need to be able to give the email an attachment from JS (e.g. a PDF).
I cannot be the first web app developer to face this and yet I'm unable to find a solution either commercial or open source.
Update:
I used the EML file method and it works well so far. Here's my JS code to create and trigger it:
var emlContent = "data:message/rfc822 eml;charset=utf-8,";
emlContent += 'To: '+emailTo+'\n';
emlContent += 'Subject: '+emailSubject+'\n';
emlContent += 'X-Unsent: 1'+'\n';
emlContent += 'Content-Type: text/html'+'\n';
emlContent += ''+'\n';
emlContent += htmlDocument;
var encodedUri = encodeURI(emlContent); //encode spaces etc like a url
var a = document.createElement('a'); //make a link in document
var linkText = document.createTextNode("fileLink");
a.appendChild(linkText);
a.href = encodedUri;
a.id = 'fileLink';
a.download = 'filename.eml';
a.style = "display:none;"; //hidden link
document.body.appendChild(a);
document.getElementById('fileLink').click(); //click the link
MSG file format is documented, but it is certainly not fun...
Why not create an EML (MIME) file?
The suggestion is to use the EML (MIME) format. According to the OP, they considered the MSG file format (#4), but were discouraged by its complexity and lack of JS libraries that process that format. If MSG file was considered, MIME is a much better choice - it is text based, so no special libraries are required to create it. Outlook will be able to open it just as easily as an MSG file.
To make sure EML message is treated as an unsent message by Outlook, set the X-Unsent MIME header to 1.
The simplest EML file would look like the following:
To: Joe The User <joe#domain.demo>
Subject: Test EML message
X-Unsent: 1
Content-Type: text/html
<html>
<body>
Test message with <b>bold</b> text.
</body>
</html>
Using the idea of plain text eml files, I came up with this: http://jsfiddle.net/CalvT/un3hapej/
This is an edit of something I found - to create a .txt file then download it. As .eml files are practically .txt files, I figured this would work. And it does. I've left the textarea with the sample email in so you can easily test. When you click on create file, it then gives you a download link to download your .eml file. The only hurdle I can see is making the browser open the .eml file after it has been downloaded.
EDIT: And thinking about it, as you have access to the client machines, you could set the browser to always open files of that type. For instance in Chrome, you can click on the arrow beside the download and select always open files of this type.
Here's the code
HTML:
(function () {
var textFile = null,
makeTextFile = function (text) {
var data = new Blob([text], {type: 'text/plain'});
if (textFile !== null) {
window.URL.revokeObjectURL(textFile);
}
textFile = window.URL.createObjectURL(data);
return textFile;
};
var create = document.getElementById('create'),
textbox = document.getElementById('textbox');
create.addEventListener('click', function () {
var link = document.getElementById('downloadlink');
link.href = makeTextFile(textbox.value);
link.style.display = 'block';
}, false);
})();
<textarea id="textbox" style="width: 300px; height: 200px;">
To: User <user#domain.demo>
Subject: Subject
X-Unsent: 1
Content-Type: text/html
<html>
<body>
Test message
</body>
</html>
</textarea>
<button id="create">Create file</button>
<a download="message.eml" id="downloadlink" style="display: none">Download</a>
Nobody seems to have answered the attachment question, so here's my solution:
create the EML as a multipart/mixed message.
Content-Type: multipart/mixed; boundary=--boundary_text_string
With this, you can have multiple parts in your email.
Multiple parts let you add attachments, like this.
Content-Type: application/octet-stream; name=demo.pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment
Start with your email headers, then add your boundary, then the part contents (newline locations are very important, clients won't parse your file correctly otherwise). You can add multiple parts. Below is an example. Note that the last boundary is different from the others (2 dashes at the end).
To: Demo-Recipient <demo#demo.example.com>
Subject: EML with attachments
X-Unsent: 1
Content-Type: multipart/mixed; boundary=--boundary_text_string
----boundary_text_string
Content-Type: text/html; charset=UTF-8
<html>
<body>
<p>Example</p>
</body>
</html>
----boundary_text_string
Content-Type: application/octet-stream; name=demo.txt
Content-Transfer-Encoding: base64
Content-Disposition: attachment
ZXhhbXBsZQ==
----boundary_text_string
Content-Type: application/octet-stream; name=demo.log
Content-Transfer-Encoding: base64
Content-Disposition: attachment
ZXhhbXBsZQ==
----boundary_text_string--
This gives you a eml file with two attachments.
See RFC 1371 if you want to know more specifics on how this works.
I had encoding problem when creating an .eml file with non-english characters and then opening it in Outlook.
The problem was that I put the "charset=" to the wrong place and did not put the quotes (") around the encoding.
The solution:
function createShiftReportEmail() {
const title = "Shift Összefoglaló";
const body = "ÁÉŐÚŰÓÜÖÍűáéúőóöí";
const emlContent = new Blob([`data:message/rfc822 eml,\nSubject: ${title}\nX-Unsent: 1\nContent-Type: text/plain;charset="utf-8"\n\n${body}`]);
if (!document.querySelector('#downloadEmail')) {
document.body.insertAdjacentHTML('beforeend', '<a id="downloadEmail" download="ShiftReport.eml" style="display: none">Download</a>');
}
const downloadBtn = document.querySelector('#downloadEmail');
downloadBtn.href = URL.createObjectURL(emlContent);
downloadBtn.click();
}
Edit: Turns out quotes (") are not even necessary. Only the placement was wrong for me.
Related
Test browser:
Version of Chrome: 52.0.2743.116
It is a simple javascript that is to open an image file from local like 'C:\002.jpg'
function run(){
var URL = "file:///C:\002.jpg";
window.open(URL, null);
}
run();
Here is my sample code.
https://fiddle.jshell.net/q326vLya/3/
Please give me any suitable suggestions.
We use Chrome a lot in the classroom and it is a must to working with local files.
What we have been using is "Web Server for Chrome". You start it up, choose the folder wishing to work with and go to URL (like 127.0.0.1:port you chose)
It is a simple server and cannot use PHP but for simple work, might be your solution:
https://chrome.google.com/webstore/detail/web-server-for-chrome/ofhbbkphhbklhfoeikjpcbhemlocgigb
1)
Open your terminal and type
npm install -g http-server
2)
Go to the root folder that you want to serve you files and type:
http-server ./
3)
Read the output of the terminal, something kinda http://localhost:8080 will appear.
Everything on there will be allowed to be got.
Example:
background: url('http://localhost:8080/waw.png');
Okay folks, I completely understand the security reasons behind this error message, but sometimes, we do need a workaround... and here's mine. It uses ASP.Net (rather than JavaScript, which this question was based on) but it'll hopefully be useful to someone.
Our in-house app has a webpage where users can create a list of shortcuts to useful files spread throughout our network. When they click on one of these shortcuts, we want to open these files... but of course, Chrome's error prevents this.
This webpage uses AngularJS 1.x to list the various shortcuts.
Originally, my webpage was attempting to directly create an <a href..> element pointing at the files, but this produced the "Not allowed to load local resource" error when a user clicked on one of these links.
<div ng-repeat='sc in listOfShortcuts' id="{{sc.ShtCut_ID}}" class="cssOneShortcutRecord" >
<div class="cssShortcutIcon">
<img ng-src="{{ GetIconName(sc.ShtCut_PathFilename); }}">
</div>
<div class="cssShortcutName">
<a ng-href="{{ sc.ShtCut_PathFilename }}" ng-attr-title="{{sc.ShtCut_Tooltip}}" target="_blank" >{{ sc.ShtCut_Name }}</a>
</div>
</div>
The solution was to replace those <a href..> elements with this code, to call a function in my Angular controller...
<div ng-click="OpenAnExternalFile(sc.ShtCut_PathFilename);" >
{{ sc.ShtCut_Name }}
</div>
The function itself is very simple...
$scope.OpenAnExternalFile = function (filename) {
//
// Open an external file (i.e. a file which ISN'T in our IIS folder)
// To do this, we get an ASP.Net Handler to manually load the file,
// then return it's contents in a Response.
//
var URL = '/Handlers/DownloadExternalFile.ashx?filename=' + encodeURIComponent(filename);
window.open(URL);
}
And in my ASP.Net project, I added a Handler file called DownloadExternalFile.aspx which contained this code:
namespace MikesProject.Handlers
{
/// <summary>
/// Summary description for DownloadExternalFile
/// </summary>
public class DownloadExternalFile : IHttpHandler
{
// We can't directly open a network file using Javascript, eg
// window.open("\\SomeNetworkPath\ExcelFile\MikesExcelFile.xls");
//
// Instead, we need to get Javascript to call this groovy helper class which loads such a file, then sends it to the stream.
// window.open("/Handlers/DownloadExternalFile.ashx?filename=//SomeNetworkPath/ExcelFile/MikesExcelFile.xls");
//
public void ProcessRequest(HttpContext context)
{
string pathAndFilename = context.Request["filename"]; // eg "\\SomeNetworkPath\ExcelFile\MikesExcelFile.xls"
string filename = System.IO.Path.GetFileName(pathAndFilename); // eg "MikesExcelFile.xls"
context.Response.ClearContent();
WebClient webClient = new WebClient();
using (Stream stream = webClient.OpenRead(pathAndFilename))
{
// Process image...
byte[] data1 = new byte[stream.Length];
stream.Read(data1, 0, data1.Length);
context.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename));
context.Response.BinaryWrite(data1);
context.Response.Flush();
context.Response.SuppressContent = true;
context.ApplicationInstance.CompleteRequest();
}
}
public bool IsReusable
{
get
{
return false;
}
}
}
And that's it.
Now, when a user clicks on one of my Shortcut links, it calls the OpenAnExternalFile function, which opens this .ashx file, passing it the path+filename of the file we want to open.
This Handler code loads the file, then passes it's contents back in the HTTP response.
And, job done, the webpage opens the external file.
Phew ! Again - there is a reason why Chrome throws this "Not allowed to load local resources" exception, so tread carefully with this... but I'm posting this code just to demonstrate that this is a fairly simple way around this limitation.
Just one last comment: the original question wanted to open the file "C:\002.jpg". You can't do this. Your website will sit on one server (with it's own C: drive) and has no direct access to your user's own C: drive. So the best you can do is use code like mine to access files somewhere on a network drive.
Chrome specifically blocks local file access this way for security reasons.
Here's an article to workaround the flag in Chrome (and open your system up to vulnerabilities):
http://www.chrome-allow-file-access-from-file.com/
There is a workaround using Web Server for Chrome. Here are the steps:
Add the Extension to chrome.
Choose the folder (C:\images) and launch the server
on your desired port.
Now easily access your local file:
function run(){
// 8887 is the port number you have launched your serve
var URL = "http://127.0.0.1:8887/002.jpg";
window.open(URL, null);
}
run();
PS: You might need to select the CORS Header option from advanced setting incase you face any cross origin access error.
This issue come when I am using PHP as server side language and the work around was to generate base64 enconding of my image before sending the result to client
$path = 'E:/pat/rwanda.png';
$type = pathinfo($path, PATHINFO_EXTENSION);
$data = file_get_contents($path);
$base64 = 'data:image/' . $type . ';base64,' . base64_encode($data);
I think may give someone idea to create his own work around
Thanks
Google Chrome does not allow to load local resources because of the security. Chrome need http url. Internet Explorer and Edge allows to load local resources, but Safari, Chrome, and Firefox doesn't allows to load local resources.
Go to file location and start the Python Server from there.
python -m SimpleHttpServer
then put that url into function:
function run(){
var URL = "http://172.271.1.20:8000/" /* http://0.0.0.0:8000/ or http://127.0.0.1:8000/; */
window.open(URL, null);
}
If you have php installed - you can use built-in server. Just open target dir with files and run
php -S localhost:8001
If you could do this, it will represent a big security problem, as you can access your filesystem, and potentially act on the data available there... Luckily it's not possible to do what you're trying to do.
If you need local resources to be accessed, you can try to start a web server on your machine, and in this case your method will work. Other workarounds are possible, such as acting on Chrome settings, but I always prefer the clean way, installing a local web server, maybe on a different port (no, it's not so difficult!).
See also:
Open local files(file://) using Chrome
Opening local files from chrome
You just need to replace all image network paths to byte strings in stored Encoded HTML string.
For this you required HtmlAgilityPack to convert Html string to Html document.
https://www.nuget.org/packages/HtmlAgilityPack
Find Below code to convert each image src network path(or local path) to byte sting.
It will definitely display all images with network path(or local path) in IE,chrome and firefox.
string encodedHtmlString = Emailmodel.DtEmailFields.Rows[0]["Body"].ToString();
// Decode the encoded string.
StringWriter myWriter = new StringWriter();
HttpUtility.HtmlDecode(encodedHtmlString, myWriter);
string DecodedHtmlString = myWriter.ToString();
//find and replace each img src with byte string
HtmlDocument document = new HtmlDocument();
document.LoadHtml(DecodedHtmlString);
document.DocumentNode.Descendants("img")
.Where(e =>
{
string src = e.GetAttributeValue("src", null) ?? "";
return !string.IsNullOrEmpty(src);//&& src.StartsWith("data:image");
})
.ToList()
.ForEach(x =>
{
string currentSrcValue = x.GetAttributeValue("src", null);
string filePath = Path.GetDirectoryName(currentSrcValue) + "\\";
string filename = Path.GetFileName(currentSrcValue);
string contenttype = "image/" + Path.GetExtension(filename).Replace(".", "");
FileStream fs = new FileStream(filePath + filename, FileMode.Open, FileAccess.Read);
BinaryReader br = new BinaryReader(fs);
Byte[] bytes = br.ReadBytes((Int32)fs.Length);
br.Close();
fs.Close();
x.SetAttributeValue("src", "data:" + contenttype + ";base64," + Convert.ToBase64String(bytes));
});
string result = document.DocumentNode.OuterHtml;
//Encode HTML string
string myEncodedString = HttpUtility.HtmlEncode(result);
Emailmodel.DtEmailFields.Rows[0]["Body"] = myEncodedString;
Chrome and other Browser restrict the access of a server to local files due to security reasons. However you can open the browser in allowed access mode. Just open the terminal and go to the folder where chrome.exe is stored and write the following command.
chrome.exe --allow-file-access-from-files
Read this for more details
This way, However, didn't work for me so I made a different route for every file in a particular directory. Therefore, going to that path meant opening that file.
function getroutes(list){
list.forEach(function(element) {
app.get("/"+ element, function(req, res) {
res.sendFile(__dirname + "/public/extracted/" + element);
});
});
}
I called this function passing the list of filename in the directory __dirname/public/extracted and it created a different route for each filename which I was able to render on server side.
This is for google-chrome-extension
const url = "file:///C:\002.jpg"
chrome.tabs.create({url, active:true})
manifest.json
{
"name": "",
"manifest_version": 3,
"permissions": [
"activeTab",
"tabs"
],
// ...
}
This solution worked for me in PHP. It opens the PDF in the browser.
// $path is the path to the pdf file
public function showPDF($path) {
if($path) {
header("Content-type: application/pdf");
header("Content-Disposition: inline; filename=filename.pdf");
#readfile($path);
}
}
I've encounterd this problem, and here is my solution for Angular, I wrapped my Angular's asset folder in encodeURIComponent() function. It worked. But still, I'd like to know more about the risk of this solution if there's any:
```const URL = ${encodeURIComponent(/assets/office/file_2.pdf)}
window.open(URL)
I used Angular 9, so this is my url when I clicked open local file:
```http://localhost:4200/%2Fassets%2Foffice%2Ffile_2.pdf```
In the case of audio files, when you give <audio src="C://somePath"/>, this throws an error saying cannot load local resource.
This makes sense because any webpage can't simply give a local path and access your private files.
In case you are trying to play audio with dynamic paths, by changing src property through JS, then here is a sample implementation using Flask server and HTML.
server.py
#app.route("/")
def home():
return render_template('audioMap.html')
#app.route('/<audio_file_name>')
def view_method(audio_file_name):
path_to_audio_file = "C:/Audios/yourFolderPath" + audio_file_name
return send_file(
path_to_audio_file,
mimetype="audio/mp3",
as_attachment=True,
attachment_filename="test.mp3")
audioMap.html
{% raw %}
<!DOCTYPE html>
<html>
<body>
AUDIO: <audio src="Std.mp3" controls >
</body>
</html>
{% endraw %}
Explanation:
When you give the audio file name under src property, this creates a get request in the flask as shown
127.0.0.1 - - [04/May/2021 21:33:12] "GET /Std.mp3 HTTP/1.1" 200 -
As you can see that, the flask has sent a Get request for the Std.mp3 file. So to serve this get request, we wrote an endpoint that takes the audio file name, reads it from the local directory, and returns it back. Hence the audio shows up on UI.
Note: This works only if you are rendering your HTML file using the
render_template method via flask or to say, using flask as your web server.
Google Chrome does not allow to load local resources because of the security .
There is a simple solution for this problem .
1.install live-server plugin in vscode
2.open the html file by live-server
I'm creating an eml file on my website as a text/html type through JavaScript for my users to download and use as a template. I want to add an attachment to this eml file so that when its downloaded the attachment is automatically attached to the email. I researched a lot on this problem before asking this question but i could not find a suitable answer/solution. So my question is, how can i possibly add an attachment to the eml file through JavaScript?
I have tried adding a link to the eml file, i.e. through the that points to the attachment, which is in the same folder as my coding files. However, i get an error saying the file could not be located when i click the link in outlook before and after its sent.
var emailTo, emailSubject, htmlDocumentContent;
emailSubject = "User Account Created";
emailTo = "xxx#example.com";
htmlDocumentContent = "html code goes here";
//creating the eml file
var emlContent = "data:message/rfc822 eml;charset=utf-8,";
emlContent += 'To: '+emailTo+'\n';
emlContent += 'Subject: '+emailSubject+'\n';
emlContent += 'X-Unsent: 1'+'\n';
emlContent += 'Content-Type: text/html'+'\n';
emlContent += ''+'\n';
emlContent += htmlDocumentContent;
var encodedUri = encodeURI(emlContent); //encode spaces etc like a url
var a = document.createElement('a'); //create a link element in the document
var linkText = document.createTextNode("fileLink"); //create the file's link to the element
a.appendChild(linkText); //append the file's link to the element
a.href = encodedUri;
a.id = 'fileLink';
a.download = 'JDE New User Email.eml';
a.style = "display:none;"; //hidden link
document.body.appendChild(a);
document.getElementById('fileLink').click(); //click the link
what i want to add to this code, or this eml file, is a pdf attachment so that when the user downloads it, it had the pdf file automatically attached to it.
Thank you very much for your help in advance.
The last answer to this question shows how to do it. I have modified it to show how you would attach a base64 encoded pdf file as an attachment...
The key step is encoding the attachment correctly for binary files.
To: Demo-Recipient <demo#demo.example.com>
Subject: EML with attachments
X-Unsent: 1
Content-Type: multipart/mixed; boundary=--boundary_text_string
----boundary_text_string
Content-Type: text/html; charset=UTF-8
<html>
<body>
<p>Example</p>
</body>
</html>
----boundary_text_string
Content-Type: application/pdf; name=example.pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment
JVBERi0xLjYNJeLjz9MNCjM3IDAgb2JqIDw8L0xpbmVhcml6ZWQgMS9MIDIwNTk3L08gNDAvRSAxNDExNS9OIDEvVCAxOTc5NS9IIFsgMTAwNSAyMTVdPj4NZW5kb2JqDSAgICAgICAgICAgICAgICAgDQp4cmVmDQozNyAzNA0KMDAwMDAwMDAxNiAwMDAwMCBuDQowMDAwMDAxMzg2IDAwMDAwIG4NCjAwMDAwMDE1MjIgMDAwMDAgbg0KMDAwMDAwMTc4NyAwMDAwMCBuDQowMDAwMDAyMjUwIDAwMDAwIG4NCjAwMDAwMDIyNzQgMDAwMDAgbg0KMDAwMDAwMjQyMyAwMDAwMCBuDQowMDAwMDAyODQ0IDAwMDAwIG4NCjAwMDAwMDI4ODggMDAwMDAgbg0KMDAwMDAwMjkzMiAwMDAwMCBuDQowMDAwMDA0MTEzIDAwMDAwIG4NCjAwMDAwMDQxNDcgMDAwMDAgbg0KMDAwMDAwNDIxMSAwMDAwMCBuDQowMDAwMDA2ODgwIDAwMDAwIG4NCjAwMDAwMDcwMjMgMDAwMDAgbg0KMDAwMDAwNzE3MiAwMDAwMCBuDQowMDAwMDA3MzEyIDAwMDAwIG4NCjAwMDAwMDc0NTUgMDAwMDAgbg0KMDAwMDAwODE3NiAwMDAwMCBuDQowMDAwMDA4NTY2IDAwMDAwIG4NCjAwMDAwMDkwNjYgMDAwMDAgbg0KMDAwMDAxMjUxOCAwMDAwMCBuDQowMDAwMDEyNjY3IDAwMDAwIG4NCjAwMDAwMTI4MDMgMDAwMDAgbg0KMDAwMDAxMjkzOSAwMDAwMCBuDQowMDAwMDEzMDcyIDAwMDAwIG4NCjAwMDAwMTMyMDggMDAwMDAgbg0KMDAwMDAxMzM0NCAwMDAwMCBuDQowMDAwMDEzNDgwIDAwMDAwIG4NCjAwMDAwMTM2MzIgMDAwMDAgbg0KMDAwMDAxMzgxOCAwMDAwMCBuDQowMDAwMDE0MDM5IDAwMDAwIG4NCjAwMDAwMDEyMjAgMDAwMDAgbg0KMDAwMDAwMTAwNSAwMDAwMCBuDQp0cmFpbGVyDQo8PC9TaXplIDcxL1ByZXYgMTk3ODQvWFJlZlN0bSAxMjIwL1Jvb3QgMzkgMCBSL0VuY3J5cHQgMzggMCBSL0luZm8gNiAwIFIvSURbPEMyMUYyMUVBNDRDMUUyRUQyNTgxNDM1RkE1QTJEQ0NFPjwxNTM0OTEwNkQ5ODVEQTQ0OTkxMDk5RjlDMENCRjAwND5dPj4NCnN0YXJ0eHJlZg0KMA0KJSVFT0YNCiAgICAgICAgICAgICAgIA0KNzAgMCBvYmo8PC9MZW5ndGggMTIzL0MgMTI4L0ZpbHRlci9GbGF0ZURlY29kZS9JIDE1MS9MIDExMi9TIDQwPj5zdHJlYW0NCjA+v2UNc4Zmn6u4IiQguoMZnwg0NH0ymtRAWZUZqNfHLCiMQS0kyMfdiuvi04hlG2GJLNJzPFccsqvk0nZ7AHI4uCBvKj3L7sGXnAk1tHgOFgzOJjqRioIZMmIdwW51On/CmLK6+gsvbo3ivOa3aWeWo5GxxRL0DzNdRQ0KZW5kc3RyZWFtDWVuZG9iag02OSAwIG9iajw8L0xlbmd0aCAyMC9GaWx0ZXIvRmxhdGVEZWNvZGUvV1sxIDEgMV0vSW5kZXhbNyAzMF0vRGVjb2RlUGFybXM8PC9Db2x1bW5zIDMvUHJlZGljdG9yIDEyPj4vU2l6ZSAzNy9UeXBlL1hSZWY+PnN0cmVhbQ0KeNpiYmJkYGJgYKQ3BggwABbZAF0NCmVuZHN0cmVhbQ1lbmRvYmoNMzggMCBvYmo8PC9MZW5ndGggMTI4L0ZpbHRlci9TdGFuZGFyZC9PKJ6imv11rrw5sF4j3R+ObJ1lZ2RcbuwZDDZAs8jdl58OFSkvUCAtMTM0MC9SIDMvVSjj41C/LnKptSQ/7nBNpOwWAAAAAAAAAAAAAAAAAAAAACkvViAyPj4NZW5kb2JqDTM5IDAgb2JqPDwvTWFya0luZm88PC9MZXR0ZXJzcGFjZUZsYWdzIDAvTWFya2VkIHRydWU+Pi9NZXRhZGF0YSA1IDAgUi9QaWVjZUluZm88PC9NYXJrZWRQREY8PC9MYXN0TW9kaWZpZWQoCM1RrvFnz1Fb5exxrGnDVyk+Pj4+L1BhZ2VzIDQgMCBSL1BhZ2VMYXlvdXQvT25lQ29sdW1uL1N0cnVjdFRyZWVSb290IDcgMCBSL1R5cGUvQ2F0YWxvZy9MYW5nKAm5TsuSKS9MYXN0TW9kaWZpZWQoCM1RrvFnz1Fb5exxrGnDVykvUGFnZUxhYmVscyAyIDAgUj4+DWVuZG9iag00MCAwIG9iajw8L0Nyb3BCb3hbMCAwIDYxMiA3OTJdL0Fubm90cyA0MSAwIFIvUGFyZW50IDQgMCBSL1N0cnVjdFBhcmVudHMgMC9Db250ZW50cyA0NiAwIFIvUm90YXRlIDAvTWVkaWFCb3hbMCAwIDYxMiA3OTJdL1Jlc291cmNlczw8L1hPYmplY3Q8PC9JbTEwIDUwIDAgUi9JbTExIDUxIDAgUi9JbTEyIDUyIDAgUi9JbTEzIDUzIDAgUi9JbTE0IDU1IDAgUi9JbTAgNTcgMCBSL0ltMSA1OCAwIFIvSW0yIDU5IDAgUi9JbTMgNjAgMCBSL0ltNCA2MSAwIFIvSW01IDYyIDAgUi9JbTYgNjMgMCBSL0ltNyA2NCAwIFIvSW04IDY1IDAgUi9JbTkgNjYgMCBSPj4vQ29sb3JTcGFjZTw8L0NTMCA0NCAwIFIvQ1MxIDQ3IDAgUi9DUzIgNDUgMCBSPj4vRm9udDw8L1RUMCA0MyAwIFI+Pi9Qcm9jU2V0Wy9QREYvVGV4dC9JbWFnZUMvSW1hZ2VJXS9FeHRHU3RhdGU8PC9HUzAgNjggMCBSPj4+Pi9UeXBlL1BhZ2U+Pg1lbmRvYmoNNDEgMCBvYmpbNDIgMCBSXQ1lbmRvYmoNNDIgMCBvYmo8PC9SZWN0WzIyMC42OCA0NjcuODggMzg5LjQ2MSA0ODMuODUyXS9TdWJ0eXBlL0xpbmsvQlM8PC9TL1MvVyAwL1R5cGUvQm9yZGVyPj4vQSA0OCAwIFIvSC9JL1N0cnVjdFBhcmVudCAxL0JvcmRlclswIDAgMF0vVHlwZS9Bbm5vdD4+DWVuZG9iag00MyAwIG9iajw8L1N1YnR5cGUvVHJ1ZVR5cGUvRm9udERlc2NyaXB0b3IgNjcgMCBSL0xhc3RDaGFyIDEyMS9XaWR0aHNbMjc4IDI3OCAwIDAgMCAwIDAgMCAzMzMgMzMzIDAgMCAyNzggMCAyNzggMjc4IDU1NiA1NTYgNTU2IDU1NiAwIDAgNTU2IDU1NiAwIDAgMjc4IDAgMCAwIDAgMCAwIDY2NyA2NjcgNzIyIDcyMiA2NjcgNjExIDAgMCAwIDAgMCAwIDAgMCAwIDY2NyAwIDAgMCA2MTEgMCAwIDk0NCAwIDY2NyAwIDAgMCAwIDAgMCAwIDU1NiA1NTYgNTAwIDU1NiA1NTYgMjc4IDU1NiA1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aWdodCAzL1R5cGUvWE9iamVjdD4+c3RyZWFtDQo3R633u5PILuNh2uqR/GEFIj0vDQplbmRzdHJlYW0NZW5kb2JqDTU5IDAgb2JqPDwvU3VidHlwZS9JbWFnZS9MZW5ndGggNy9CaXRzUGVyQ29tcG9uZW50IDgvQ29sb3JTcGFjZSA0NyAwIFIvV2lkdGggMS9IZWlnaHQgMi9UeXBlL1hPYmplY3Q+PnN0cmVhbQ0K14/pGKyXxA0KZW5kc3RyZWFtDWVuZG9iag02MCAwIG9iajw8L1N1YnR5cGUvSW1hZ2UvTGVuZ3RoIDcvQml0c1BlckNvbXBvbmVudCA4L0NvbG9yU3BhY2UgNDcgMCBSL1dpZHRoIDIvSGVpZ2h0IDEvVHlwZS9YT2JqZWN0Pj5zdHJlYW0NCrN3Ds5BHkkNCmVuZHN0cmVhbQ1lbmRvYmoNNjEgMCBvYmo8PC9TdWJ0eXBlL0ltYWdlL0xlbmd0aCA0L0JpdHNQZXJDb21wb25lbnQgOC9Db2xvclNwYWNlIDQ3IDAgUi9XaWR0aCAxL0hlaWdodCAxL1R5cGUvWE9iamVjdD4+c3RyZWFtDQrx8DB4DQplbmRzdHJlYW0NZW5kb2JqDTYyIDAgb2JqPDwvU3VidHlwZS9JbWFnZS9MZW5ndGggNy9CaXRzUGVyQ29tcG9uZW50IDgvQ29sb3JTcGFjZSA0NyAwIFIvV2lkdGggMi9IZWlnaHQgMS9UeXBlL1hPYmplY3Q+PnN0cmVhbQ0K9/8xai1TaA0KZW5kc3RyZWFtDWVuZG9iag02MyAwIG9iajw8L1N1YnR5cGUvSW1hZ2UvTGVuZ3RoIDcvQml0c1BlckNvbXBvbmVudCA4L0NvbG9yU3BhY2UgNDcgMCBSL1dpZHRoIDIvSGVpZ2h0IDEvVHlwZS9YT2JqZWN0Pj5zdHJlYW0NCiN83Skmd7gNCmVuZHN0cmVhbQ1lbmRvYmoNNjQgMCBvYmo8PC9TdWJ0eXBlL0ltYWdlL0xlbmd0aCA3L0JpdHNQZXJDb21wb25lbnQgOC9Db2xvclNwYWNlIDQ3IDAgUi9XaWR0aCAyL0hlaWdodCAxL1R5cGUvWE9iamVjdD4+c3RyZWFtDQpDpbHuCaveDQplbmRzdHJlYW0NZW5kb2JqDTY1IDAgb2JqPDwvU3VidHlwZS9J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MDAwMDAwIDY1NTM1IGYNCjAwMDAwMTQxMTUgMDAwMDAgbg0KMDAwMDAxNDk2NSAwMDAwMCBuDQowMDAwMDE0OTk4IDAwMDAwIG4NCjAwMDAwMTUwMjEgMDAwMDAgbg0KMDAwMDAxNTA3MiAwMDAwMCBuDQowMDAwMDE5NDg0IDAwMDAwIG4NCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAwMCA2NTUzNSBmDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAwMCA2NTUzNSBmDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAwMCA2NTUzNSBmDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAwMCA2NTUzNSBmDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAwMCA2NTUzNSBmDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAwMCA2NTUzNSBmDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAwMCA2NTUzNSBmDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAwMCA2NTUzNSBmDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAwMCA2NTUzNSBmDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAwMCA2NTUzNSBmDQowMDAwMDAwMDAwIDY1NTM1IGYNCnRyYWlsZXINCjw8L1NpemUgMzcvRW5jcnlwdCAzOCAwIFI+Pg0Kc3RhcnR4cmVmDQoxMTYNCiUlRU9GDQo=
----boundary_text_string
Content-Type: application/octet-stream; name=demo.log
Content-Transfer-Encoding: base64
Content-Disposition: attachment
ZXhhbXBsZQ==
----boundary_text_string--
Given https://www.example.com/image-list:
...
<a href="/image/1337">
<img src="//static.example.com/thumbnails/86fb269d190d2c85f6e0468ceca42a20.png"/>
</a>
<a href="//static.example.com/full/86fb269d190d2c85f6e0468ceca42a20.png"
download="1337 - Hello world!.png">
Download
</a>
...
This is a user script environment, so I have no access to server configuration. As such:
I can't make server accept user-friendly file names like https://static.example.com/full/86fb269d190d2c85f6e0468ceca42a20 - 1337 - Hello World!.png.
I can't configure Cross-Origin Resource Sharing. www.example.com and static.example.com are separated by CORS wall by design.
How to make Firefox and Chrome display Save File As dialog with the suggested file name "1337 - Hello world!.png" when a user clicks on the "Download" link?
After some failing and googling, I learned these problems:
Firefox completely ignores existence of the download attribute on some image MIME types.
Firefox completely ignores existence of the download attribute on cross-site links.
Chrome completely ignores value of the download attribute on cross-site links.
All these points don't make any sense to me, all look like "let's put random non-sensical limitations on the feature", but I have to accept them as it's my environment.
Do any ways to solve the problem exist?
Background: I'm writing a user script for an image board which uses MD5 hashes as file names. I want to make saving with user-friendly names easier. Anything which gets me closer to this would be helpful.
I guess I can get around the limitations by using object URLs to blobs and a local proxy with hacked CORS headers, but this setup is obviously beyond reasonable. Saving through canvas could work (are images "protected" by CORS in this case too?), but it will either force double lossy compression or lossy-to-lossless conversion, given JPEG files, neither of which are good.
All modern browsers will ignore the download attribute in the anchor tag for cross-origin URL'S.
Reference : https://html.spec.whatwg.org/multipage/links.html#downloading-resources
According to the spec makers, this represents a security loophole as a user could be tricked into downloading malicious files while browsing a secure site, believing that the file is also originating from the same secure site.
Any interesting conversation for implementing this feature in the firefox browser can be found here : https://bugzilla.mozilla.org/show_bug.cgi?id=676619
[ Edit by Athari ]
Quote from specification:
This could be dangerous, because, for instance, a hostile server could be trying to get a user to unknowingly download private information and then re-upload it to the hostile server, by tricking the user into thinking the data is from the hostile server.
Thus, it is in the user's interests that the user be somehow notified that the resource in question comes from quite a different source, and to prevent confusion, any suggested filename from the potentially hostile interface origin should be ignored.
Clarification on the mysterious scenario:
the more serious issue with CORS downloads is if a malicious site forces a download of a file form a legitimate site and some how gets access to its content. so lets say I download the user gmail inbox page and explore its messages.
in this case an evil site will have to fool the user into downloading the file and uploading it back to the server, so lets say we have a gmail.com/inbox.html actually contains all the user mail messages and the attacker sites offers a download link for a coupon file, that should be uploaded to another evil site. the coupon will supposedly offer a 30% discount on a new Ipad. the download link will actually point to gmail.com/inbox.html and will download it as "30off.coupon", the if the user will download this file and upload it without checking it's content the evil site will get the user "coupon" and so its inbox content.
Important notes:
Google originally didn't limit download attribute by CORS and was explicitly against this. It was later forced to adjust Chrome implementation.
Google was opposed to using CORS for this.
Alternative solutions were proposed with giving a user a warning about cross-origin downloads. They were ignored.
Well there can be notification or deny/allow mechanism when downloading from another origin (e.g. like in case of geolocation API). Or not to send cookies in case of cross origin request with download attribute.
Some developers do share the opinion that the restriction is too strong, severely limits the usage of the feature and that the scenario is so complicated that the user who would do this would easily download and run an executable file. Their opinion was disregarded.
The case against allowing cross-origin downloads is centered around the premise that visitors of an [evil] site (eg, discountipads.com) could unknowingly download a file from a site containing their own personal information (eg, gmail.com) and save it to their disk using a misleading name (eg, "discount.coupon") AND THEN proceed to another malicious page where they manually upload that same file they just downloaded. This is quite far-fetched in my opinion, and anyone who would succumb to such trivial trickery perhaps does not belong online in the first place. I mean c'mon...Click here to download our special discount offer and then re-upload it through our special form! Seriously? Download our special offer and then email it to this Yahoo address for a big discount! Do the people who fall for these things even know how to do email attachments?
I'm all for browser security, but if the good people of Chromium have no problem with this I don't see why Firefox has to completely banish it. At the very least I'd like to see a preference in about:config to enable cross-origin #download for "advanced" users (default it to false). Even better would be a confirmation box similar to: "Although this page is encrypted, the information you submit through this form won't be" or: "This page is requesting to install addons" or: "Files downloaded from the web may harm your computer" or even: "The security certificate of this page is invalid" ...y'know what I mean? There are myriad ways to heighten the user's awareness and inform them this might not be safe. One extra click and a short (or long?) delay is enough to let them assess the risk.
As the web grows, and the use of CDNs grows, and the presence of advanced web-apps grows, and the need to manage files hosted across servers grows, features like #download will become more important. And when a browser like Chrome supports it fully whereas Firefox does not, this is not a win for Firefox.
In short, I think that mitigating the potential evil uses of #download by simply ignoring the attribute in cross-origin scenarios is a woefully ill-thought move. I'm not saying the risk is entirely non-existent, quite the contrary: I am saying there are plenty of risky things one does online in the course of his day...downloading ANY file is high among them. Why not work around that issue with a well-thought user experience?
Overall, considering widespread use of CDNs and intentionally putting user-generated content on a different domain, the primary use for the download attribute is specifying a file name for blob downloads (URL.createObjectURL) and the like. It can't be used in a lot of configurations and certainly not very useful in user scripts.
Try something like:
Get the external image to your server first
Return the fetched image from your server.
Dynamically create an anchor with download name and .click() it!
while the above was just a pretty short tips list... give this a try:
on www.example.com place a fetch-image.php with this content:
<?php
$url = $_GET["url"]; // the image URL
$info = getimagesize($url); // get image data
header("Content-type: ". $info['mime']); // act as image with right MIME type
readfile($url); // read binary image data
die();
or with any other server-side language that achieves the same.
The above should return any external image as it's sitting on your domain.
On your image-list page, what you can try now is:
<a
href="//static.example.com/thumbnails/86fb269d190d2c85f6e0468ceca42a20.png"
download="1337 - Hello world!.png">DOWNLOAD</a>
and this JS:
function fetchImageAndDownload (e) {
e.preventDefault(); // Prevent browser's default download stuff...
const url = this.getAttribute("href"); // Anchor href
const downloadName = this.download; // Anchor download name
const img = document.createElement("img"); // Create in-memory image
img.addEventListener("load", () => {
const a = document.createElement("a"); // Create in-memory anchor
a.href = img.src; // href toward your server-image
a.download = downloadName; // :)
a.click(); // Trigger click (download)
});
img.src = 'fetch-image.php?url='+ url; // Request image from your server
}
[...document.querySelectorAll("[download]")].forEach( el =>
el.addEventListener("click", fetchImageAndDownload)
);
You should see finally the image downloaded as
1337 - Hello world!.png
instead of 86fb269d190d2c85f6e0468ceca42a20.png like it was the case.
Notice: I'm not sure about the implications of simultaneous requests toward fetch-image.php - make sure to test, test.
If you have access to both, backend and frontend code, here are steps which could help you
I'm not sure which type of backend language you are using, so I will just explain what need to be done without code sample.
In backend, for preview your code should work as is, if you get in query string something like ?download=true then your backend should pack file as dispositioned content, in other words you would use content-disposition response header. This will open you possibility to put additional attributes to content, like filename, so it could be simething like this
Content-Disposition: attachment; filename="filename.jpg"
Now, in frontent, any link which should behave as download button need to contain ?download=true in href query parameter AND target="_blank" which will temporary open another tab in browser for download purpose.
<a href="/image/1337">
<img src="//static.example.com/thumbnails/86fb269d190d2c85f6e0468ceca42a20.png"/>
</a>
<a href="//static.example.com/full/86fb269d190d2c85f6e0468ceca42a20.png?download=true" target="_blank" title="1337 - Hello world!.png">
Download Full size
</a>
I know that this works without CORS setup and if user clicks on download link, but I never tested Save As dialog in browser... and it will take some time to build this again, so please give it a try.
Relevant Chromium API...
https://developer.chrome.com/extensions/downloads#event-onDeterminingFilename
Example...
chrome.downloads.onDeterminingFilename.addListener(function(item,suggest){
suggest({filename:"downloads/"+item.filename}); // suggest only the folder
suggest({filename:"downloads/image23.png"}); // suggest folder and filename
});
Oops... you're on server side but I assumed client side! I'll leave this here though in case someone needs it.
You can try to do this
var downloadHandler = function(){
var url = this.dataset.url;
var name = this.dataset.name;
// by this you can automaticaly convert any supportable image type to other, it is destination image format
var mime = this.dataset.type || 'image/jpg';
var image = new Image();
//We need image and canvas for converting url to blob.
//Image is better then recieve blob through XHR request, because of crossOrigin mode
image.crossOrigin = "Anonymous";
image.onload = function(oEvent) {
//draw image on canvas
var canvas = document.createElement('canvas');
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
canvas.getContext('2d').drawImage(this, 0, 0, canvas.width, canvas.height);
// get image from canvas as blob
var binStr = atob( canvas.toDataURL(mime).split(',')[1] ),
len = binStr.length,
arr = new Uint8Array(len);
for (var i = 0; i < len; i++ ) {
arr[i] = binStr.charCodeAt(i);
}
var blob = new Blob( [arr], {type: mime} );
//IE not works with a.click() for downloading
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(blob, name);
} else {
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = name;
a.click();
}
};
image.src = url;
}
document.querySelector("[download]").addEventListener("click", downloadHandler)
<button
data-name="file.png"
data-url="https://tpc.googlesyndication.com/simgad/14257743829768205599"
data-type="image/png"
download>
download
</button>
Another modern way for modern browsers (except Internet Explorer)
var downloadHandler = function(){
var url = this.dataset.url;
var name = this.dataset.name;
fetch(url).then(function(response) {
return response.blob();
}).then(function(blob) {
//IE and edge not works with a.click() for downloading
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(blob, name);
} else {
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = name;
a.click();
}
});
};
document.querySelector("[download]").addEventListener("click", downloadHandler)
<button
data-name="file.png"
data-url="https://tpc.googlesyndication.com/simgad/14257743829768205599"
download>
download
</button>
I'm able to rename base64 images with a save as input field.
I think your best bet is to create your own "save as" box. When a user clicks "download", display a "File Name: {input field}" and a save button. Have the save button change filename and then call the download function.
function save2() {
var gh = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADNQTFRFAAAAPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09EvTTBAAAABB0Uk5TAA8fLz9PX29/j5+vv8/f7/rfIY4AAARsSURBVHja7VvZkusqDAyrAQvQ/3/teZjExrHDZoGrbl0/z0RtLY1oya/X/8+Nh/NHzYuAGPRz9llERET1GACJiIgYngaAkmvNnwTgERHtgwDefngawDofgDkAwPlp4I4AzHQA/gggstk0jF/P7ELQ3wBQPBoBRPRzTyI8P/bBGvh79FMstFXCvDSAt0kHzyBYNv7jj/iAx48DEiwzEfj0AFi/EEw4F+2B/5mfXQsbB4ZDQOKGwM2ioE+4hUdEm3jCjzybRbw4gIXkrxfbySnqCfYhS48rG23fs/wRGYdcGIQv1PsOcIgTkp//xTcs4WbyTEjs67pmFfh8+3+X1s0Jy3z7rxezaZ9EdTjI2MC1MpA37LqN65kjdoJuPmtUUpC40NmvLy2WntM3OcH09RupE8KdMLjefufgBE1gvz2blnj/2pDY7wikSPold9M+dCVSWpDuln1HUMLuCfsHEndP2H+9uO+kJEfVaicNq+zin9udxY6gQcrRlFeNHUG1oCfpjpIjAtmaukQXHRabpJwdMNlFSzZFdL3Dv4WkrlH4lyH6Y6jOgj0BSPUGWV0InrQAztISr2UgahFe1r3XJgHRC9C+qhK3CqC/4H6Sm1XV64ApCKt5NegOgFTGGGPMIlnhx22NA64zhUsppTxVMtcuvY5hcCqX31DhgAu+EgZ+WLjSjoPJvF6mBH5lIFvC7wHBJ7kAAAByjFdkAvdDg0o7/PPByiOCSSIvbfhBo6HExvES/ftwjOs7v7iyoZCl0qhMhHWpDQoX9QvH/xJd+osriAbr9ZktEQONCm3yAD5EEU833YWIlgsA1PD5UwGAGz4DLAAIw0eAeQBs/CTaZi2o8VNYyAIwP2qAHsCSZYGR6xD5xtgPTwGeBzB+I0Xlj+Oajo2kCEK+GRqfg2sWwEAaKhCNLDdsRCkgnwLg8kEeDyDmLQwHoAp3w+EA1kJPPBoAL6lEYnAZmuLtfCwRbToZLwEYNP7X5Vs33NEFuI15BS6U7+auuydmGkoKXI1Kt9RlIZPHIIllLbfzWwboCm2AF480b7WUQkipDWySkhPlg7ggU9apWPFqkWzV2TZC1Am1a1UMltMWW8F6Xve4qpRCX86U3ZQkcEtFF79UKtW8RSJnsvr+IDK7N23HRScH+mrtWQ/RCF3D+DYOaM337bOKftvQ78iKps3fjbDIrkeX22cVLqAKAovVFfD1DzRi/V4AgbWmDMW8ivmO7Qto9FlV/FvGr5xsZilj3/hXI00UTPcKi6PYgkrXR5qnb/72ZuRho03fSF5E1xOGg7qvb5VPz2akTmcbnT48LExDCysycxitdGfRcWUbar2gvj59cDfqyH3NoMpNyt+k5r77t1B+tb/eZNzJtTt1y+4umXM49b9g1AmFUPvloDdzqsppDweA/RuSOoDLv6D7GvRAKPUP5ceo3DWbX4nFXm5iy8ubEfqCWiut22HDDqZcyBuP6zL6s0euLVzbBqunfWbFpTZmhfdjjVFy9seO/6nnH0Mpp/3TjvofAAAAAElFTkSuQmCC"
var a = document.createElement('a');
a.href = gh;
a.download = document.getElementById("input").value;
a.click()
}
Filename: <input id="input" name="input" placeholder="enter image name"></insput><button onclick="save2()">Save</button>
The above code will let you name the file for the user If you want the user to be able to name the file himself then you need to create an input field that updates a.download = 'imagetest.png'; through a listening function or onkeychange. I got it to work through "a.download = document.getElementById("input").value;". It's amazing what getElementByID can do.
Browsers are very limited in the ability to name files for people to download. I imagine it's a lack of functionality that has been overlooked.
If you want to take it a step further you can hide the "file name:" input field with display:hidden;. And you can have a "download" button with an onclick function that sets the "file name: input / save" div to no longer set to display:hidden;. This can also be done through getelementbyID.
I did however notice that there seems to be an issue with renaming the image with a URL instead of base64. So I tried integrating my code with yours though none of the code below seemed to work properly when clicking on your download link. I still think it's close enough that you may want to fiddle around with it if this is the route you want to go:
<script>
function save2() {
var gh = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"
var a = document.createElement('a');
a.href = gh;
a.download = document.getElementById("input").value;
a.click()
}
function downloadit(){
var x = document.getElementById("input").value;
document.getElementById("dl").getAttribute("download") = x;
}
document.getElementById("dl").onclick = function() {
document.getElementById("dl").download= a.download = document.getElementById("input").value;
return false;
}
</script>
<a href="/image/1337">
<img src="//static.example.com/thumbnails/86fb269d190d2c85f6e0468ceca42a20.png"/>
</a>
<a onclick='downloadit()' id="dl" href="//static.example.com/full/86fb269d190d2c85f6e0468ceca42a20.png"
download="1337 - Hello world!.png">
Download
</a>
Filename: <input id="input" name="input" placeholder="enter image name"></insput><button onclick="save2()">Save</button>
Lastly, I was able to rename your example.com image with the following script. Though when I try it with a working googleimage it doesn't seem to rename. So you might want to dabble with this as well.
document.getElementById("dl").onclick = function() {
document.getElementById("dl").download=document.getElementById("input").value;
}
Filename: <input id="input" name="input" placeholder="enter image name"></input>
<a id="dl" href="//static.example.com/full/86fb269d190d2c85f6e0468ceca42a20.png"
download="1337 - Hello world!.png">
Download
</a>
I have a problem (or may be two) with saving files using HTML5 File API.
A files comes from the server as a byte array and I need to save it. I tried several ways described on SO:
creating blob and opening it in a new tab
creating a hidden anchor tag with "data:" in href attribute
using FileSaver.js
All approaches allow to save the file but with breaking it by changing the encoding to UTF-8, while the file (in current test case) is in ANSI. And it seems that I have to problems: at the server side and at the client side.
Server side:
Server side is ASP.NET Web API 2 app, which controller sends the file using HttpResponseMessage with StreamContent. The ContentType is correct and corresponds with actual file type.
But as can be seen on the screenshot below server's answer (data.length) is less then actual file size calculated at upload (file.size). Also here could be seen that HTML5 File object has yet another size (f.size).
If I add CharSet with value "ANSI" to server's response message's ContentType property, file data will be the same as it was uploaded, but on saving result file still has wrong size and become broken:
Client side:
I tried to set charset using the JS File options, but it didn't help. As could be found here and here Eli Grey, the author of FileUplaod.js says that
The encoding/charset in the type is just metadata for the browser, not an encoding directive.
which means, if I understood it right, that it is impossible to change the encoding of the file.
Issue result: at the end I can successfully download broken files which are unable to open.
So I have two questions:
How can I save file "as is" using File API. At present time I cannot use simple way with direct link and 'download' attribute because of serverside check for access_token in request header. May be this is the "bottle neck" of the problem?
How can I avoid setting CharSet at server side and also send byte array "as is"? While this problem could be hacked in some way I guess it's more critical. For example, while "ANSI" charset solves the problem with the current file, WinMerge shows that it's encoding is Cyrillic 'Windows-1251' and also can any other.
P.S. the issue is related to all file types (extensions) except *.txt.
Update
Server side code:
public HttpResponseMessage DownloadAttachment(Guid fileId)
{
var stream = GetFileStream(fileId);
var message = new HttpResponseMessage(HttpStatusCode.OK);
message.Content = new StreamContent(stream);
message.Content.Headers.ContentLength = file.Size;
message.Content.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType)
{
// without this charset files sent with bigger size
// than they are as shown on image 1
CharSet = "ANSI"
};
message.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
FileName = file.FileName + file.Extension,
Size = file.Size
};
return message;
}
Client side code (TypeScript):
/*
* Handler for click event on download <a> tag
*/
private downloadFile(file: Models.File) {
var self = this;
this.$service.downloadAttachment(this.entityId, file.fileId).then(
// on success
function (data, status, headers, config) {
var fileName = file.fileName + file.extension;
var clientFile = new File([data], fileName);
// here's the issue ---^
saveAs(clientFile, fileName);
},
// on fail
function (error) {
self.alertError(error);
});
}
My code is almost the same as in answers on related questions on SO: instead of setting direct link in 'a' tag, I handle click on it and download file content via XHR (in my case using Angularjs $http service). Getting the file content I create a Blob object (in my case I use File class that derives from Blob) and then try to save it using FileSaver.js. I also tried approach with encoded URL to Blob in href attribute, but it only opens a new tab with a file broken the same way. I found that the problem is in Blob class - calling it's constructor with 'normal' file data I get an instance with 'wrong' size as could be seen on first two screenshots. So, as I understand, my problem not in the way I try to save my file, but in the way I create it - File API
i have a link that allows me to downlowd a text file in my web page but the problem is that i want the user to be able to chose where to save the file i mean when he clicks the link a window should be opened so he can save the file wherever he likes can any one tell me how to do that? thx .
here is a part of my code:
$fichierres=fopen('res.txt','a');
ftruncate($fichierres,0);
...
fputs($fichierres, $t."\r\n");
...
fclose($fichierres);
echo' <div style="text-align:center"><br> <button id="download" width="100px" class="styled-button-8"><b>Download</b></button></div><br>';
Most browsers will auto-open any file that they can read - exactly how they should work. This includes .txt files, there's nothing that you can do to prevent this.
What you can do is provide the link as an anchor (Download) and provide a message next to the link telling the user to "Right click / Save link as..." to download - this will allow them to save the file rather than download.
The exact option in the right click menu will differ between browsers but it's always something along the lines of "Save Link As...".
http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
19.5.1 Content-Disposition
The Content-Disposition response-header field has been proposed as a
means for the origin server to suggest a default filename if the user
requests that the content is saved to a file. This usage is derived
from the definition of Content-Disposition in RFC 1806 [35].
content-disposition = "Content-Disposition" ":"
disposition-type *( ";" disposition-parm )
disposition-type = "attachment" | disp-extension-token
disposition-parm = filename-parm | disp-extension-parm
filename-parm = "filename" "=" quoted-string
disp-extension-token = token
disp-extension-parm = token "=" ( token | quoted-string )
An example is
Content-Disposition: attachment; filename="fname.ext"
In PHP, you would use header function to send this header. Note that this must be called before any data is sent.
header('Content-Disposition: attachment; filename="fname.ext"');