Error opening excel generated with EPPlus in medium trust environment - c#

I have searched high and low and find similar questions all with similar answers and none seem to solve my problem. My ultimate goal is to allow the user to save a datatable to an excel document which opens (not saves) in excel and they can choose to save it if they wish. This is to be done with the click of a button on a webpage written in c#.
There's tons of examples on how to do this, in many different ways, many of which seem quite straightforward - I just can't seem to find a way that completely works in the environment I am in (medium trust production environment and this cannot be altered).
Essentially, when a button is clicked on the webpage, my code creates an excel file using EPPlus:
ExcelPackage p = new ExcelPackage();
p.Workbook.Worksheets.Add(worksheetname);
ExcelWorksheet workSheet = p.Workbook.Worksheets[1];
workSheet.Cells[1,1].LoadFromDataTable(dt, true);
var range = workSheet.Cells[1,1,dt.Rows.Count,dt.Columns.Count];
var table = workSheet.Tables.Add(range, "results");
table.ShowTotal = false;
table.TableStyle = TableStyles.Medium4;
workSheet.Cells.AutoFitColumns();
workSheet.Cells.Style.HorizontalAlignment = ExcelHorizontalAlignment.Left;
workSheet.View.ShowGridLines = false;
Response.Clear();
Response.ClearHeaders();
Response.BinaryWrite(p.GetAsByteArray());
//Also tried this instead of above line with same error: p.SaveAs(Response.OutputStream);
Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
Response.AddHeader("content-disposition", "attachment; filename=" + filename + ".xlsx");
Response.End();
This code seems straightforward, and works locally from my machine. I click the button, then receive a prompt to Save or Open. I choose Open, and the file opens in Excel, which is exactly what my end users want. I then copy the website out to our dev web server(full trust) and it still works great. Then I copy to our staging environment which is medium trust, not full trust and get the following error: System.Security.SecurityException Exception Message: Request for the permission of type 'System.Security.Permissions.FileIOPermission, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
I am not trying to save a file, just simply display for the end user to be able to open it in Excel. They can then choose to Save-As from Excel app if they wish.
Doing this with Excel interop is straightforward and I got it working locally as well, but without Excel on the server, I don't believe this is a viable option. Hence my decision to try EPPlus, which seemed like the answer until it wouldn't work in our staging environment. The only difference I can think of is that our staging environment is medium trust and the development environment is full trust. I need a solution that works in medium trust, but without the errors of my other option I tried below using html.
I used to do this as html and open with the following code, but since it isn't a true excel file, when the user selects open, they always got the error: "The file format and extension don't match. The file could be corrupted or unsafe. Unless you trust its source, don't open it. Do you want to open it anyway?". They could just hit yes and most of the time the file would open and behave just fine, other times it will simply say "Unable to read file" and they would have to re-click the button - they have requested this error go away. Here is how I used to do it (and again, this worked most the time, and works just fine in our medium trust environment, but has that pesky error). This is why I decided to go the route of creating a 'true' excel file-to avoid this error.
System.IO.StringWriter tw = new System.IO.StringWriter();
System.Web.UI.HtmlTextWriter hw = new System.Web.UI.HtmlTextWriter(tw);
DataGrid dgGrid = new DataGrid();
dgGrid.DataSource = dt;
dgGrid.DataBind();
dgGrid.RenderControl(hw);
Response.Clear();
Response.ClearContent();
Response.ClearHeaders();
Response.Buffer = true;
Response.Expires = 0;
Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
//Also tried these two ways to see if the "file format and extension don't match" error go away-it didn't
//also tried sending in filename with .xls and .xlsx extension with same results
//Response.ContentType = "application/vnd.ms-excel";
//Response.ContentType = "application/octet-stream";
Response.AppendHeader("Content-Disposition", "attachment; filename=" + filename);
this.EnableViewState = false;
Response.Write(tw.ToString());
Response.Flush();
Response.Clear();
Response.End();
Any suggestions would be great! Also, if there is another open source tool which may do the job, that would be good too. I cannot use any and all open source options, but if someone has a suggestion on another tool that would work in medium trust, I can see if it's in my company's approved list.

The issue is that EPPlus is using features from System.IO even when using the constructors that take a memory stream. That particular library needs full trust, so it will not run in a medium trust environment. Have a look at the following two files:
http://epplus.codeplex.com/SourceControl/latest#EPPlus/Packaging/ZipPackage.cs
string name = Path.GetFileName(p.Key);
string extension = Path.GetExtension(p.Key);
http://epplus.codeplex.com/SourceControl/latest#EPPlus/Packaging/ZipPackagePart.cs
internal void WriteZip(ZipOutputStream os)
{
...code...
if (_rels.Count > 0)
{
string f = Uri.OriginalString;
var name = Path.GetFileName(f);
_rels.WriteZip(os, (string.Format("{0}_rels/{1}.rels", f.Substring(0, f.Length - name.Length), name)));
}
...code...
}
Both of those sections use the Path class from System.IO to perform some basic tasks.
ZipPackage.cs solution:
var parts = p.Key.Split('/');
var name = parts[parts.Length - 1];
parts = name.Split('.');
var extension = "." + parts[parts.Length - 1];
ZipPackagePart.cs solution:
internal void WriteZip(ZipOutputStream os)
{
...code...
if (_rels.Count > 0)
{
string f = Uri.OriginalString;
var parts = f.Split('/');
var name = parts[parts.Length - 1];
_rels.WriteZip(os, (string.Format("{0}_rels/{1}.rels", f.Substring(0, f.Length - name.Length), name)));
}
...code...
}
There may be other places where this issue presents itself, but this should give you an idea of what you need to look for and change after pulling the source code in to your solution. Best of luck!

Related

How do I export a C#/asp.net datagrid to Excel and avoid the new security preventing opening Excel files?

I have a C#/asp.net program that stores data in a datagrid and then uses HttpResponse to create an Excel file from it. This worked well until Microsoft made security changes. Here's some information on that change: http://www.networksteve.com/exchange/topic.php/XLS_file_will_not_open_in_Excel_2016,_only_gray_view/?TopicId=77375&Posts=3. The solutions such as turning off Excel security and uninstalling the patch the caused the problem are not acceptable for my client. The official Microsoft answer is "The best option is to move away from using HTML wrapped as .XLS. If you use native formats (e.g. XLS, XLSX, XLSB) which will open in protected view when untrusted, this will provide some level of protection from the documents being opened." http://answers.microsoft.com/en-us/office/forum/office_2010-excel/windows-installed-updates-and-now-downloaded-excel/069b0fdf-d085-4322-b298-5976d0efa9ce?rtAction=1468539460673
My question is, how do I do that? I tried using the Microsoft.Interop and found multiple people saying this is not a good solution. I could never get that working anyway; see that discussion here: How can I download an Excel file to the user's download folder?
See my code below. I'm hoping that I can change the header or content type and get this to work. I'm open to any coding suggestions. As I said, our client is not going to change security options on their and their clients' machines; this needs to be something I can do in the code so the client doesn't have to do anything. Also, my boss is very hesitant to give me time to learn 3rd party controls so those are probably out as well.
var grid = new DataGrid();
grid.HeaderStyle.Font.Bold = true;
grid.AlternatingItemStyle.BackColor = System.Drawing.Color.DarkGray;
grid.DataSource = GetTable(storerId, bSunday, bMonday, bTuesday, bWednesday, bThursday, bFriday, bSaturday, beginDate, endDate);
grid.DataBind();
response.Clear();
//response.AddHeader("content-disposition", string.Format("{0}{1}{2}{3}{4}", "attachment;filename=", storerId, "KpiExport", DateTime.Today.ToShortDateString(), ".xls"));
//response.AddHeader("Content-Disposition", string.Format("{0}{1}{2}{3}{4}", "attachment;filename=", storerId, "KpiExport", DateTime.Today.ToShortDateString(), ".xls"));
HttpContext.Current.Response.AddHeader("content-disposition", string.Format("attachment; filename={0}", "sample.xls"));
response.Charset = "";
// response.ContentType = "application/vnd.xls";
response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
var writer = new StringWriter();
var htmlWriter = new HtmlTextWriter(writer);
grid.RenderControl(htmlWriter);
response.Write(writer.ToString());
response.End();

How to disable file download popup and save file directly to C drive

My program generates doc file on browser and i want to save this file to my C drive dynamically
string filename = "fileName";
Response.Clear();
Response.Buffer = true;
Response.AddHeader("content-disposition", "attachment;filename= " + filename +".doc" );
Response.Charset = "";
Response.ContentType = "application/vnd.ms-word ";
StringWriter sw = new StringWriter();
HtmlTextWriter hw = new HtmlTextWriter(sw);
GridView1.AllowPaging = false;
GridView1.DataBind();
GridView1.RenderControl(hw);
Response.Output.Write(sw.ToString());
Response.Flush();
Response.End();
You cannot decide for the user where to save the file. User decides that.
You can save a given location on the server, but you can't tell a client where to save a file in the HTTP response. Beyond the security problems inherent in that approach, you have to take into account that there are a diverse array of clients with different file structures. Even Windows clients do not necessarily have a C: drive. A few ActiveX controls could allow you to dictate a save location, but those are client-side code and recent version of Internet Explorer intentionally make that difficult to implement. Also, if the user is using some sort of download client, you can pre-populate a location, but if you don't know their file structure you're still going to have trouble.

Exporting Gridview to Excel - External table is not in the expected format

This is my code, but I end up with an Unexpected format error. Something must be wrong with the MIME-type, however I found this one as being te original official MIME-type.
What am I missing? Each time I try to open any document I create by this method seems to be "corrupt", but when I click the message away everything works fine.
Message: The file you are trying to open, 'name.ext', is in a different format than specified by the file extension. Verify that the file is not corrupted and is from a trusted source before opening the file. Do you want to open the file now?
However, I need to get rid of this warning, since I cannot import the file because it's invalid. If I save the file as 2003-2007 format .xls, it seems to be fixed. But this is not a working solution.
protected void ExportToExcel(string fileName)
{
var cmd = new SqlCommand(query);
var dt = GetData(cmd);
var writer = new StringWriter();
var gridview = new GridView();
gridview.AllowPaging = false;
gridview.DataSource = dt;
gridview.DataBind();
Response.Clear();
var hw = new HtmlTextWriter(writer);
gridview.RenderControl(hw);
Response.ContentType = "application/vnd.ms-excel";
Response.AddHeader("Content-Disposition", "attachment;filename=" + fileName + ".xls");
// this.EnableViewState = false;
Response.Write(writer.ToString());
// Response.Flush();
Response.End();
}
To be direct, you are not doing excel export. You are sending html table with fake response headers to trick the browser to open this content with excel.
That can fail in many ways, in fact can be interpreded as malicious behaviour. I think that going this way is only calling for troubles.
Better way is to use native excel library and do export with it. Here is the example of exporting DataTable to real excel file (2007+)
https://stackoverflow.com/a/9569827/351383
Thought I'd add a comment for future reference. One of my macros that was doing a TransferSpreadsheet (export) started failing, giving the "External table is not in the expected format." error window. I went to the path of the destination file and opened it. Found that the last file created contained 'garbage' characters. I deleted the Excel file and ran my macro again with no issue.

How to show save as dialog box in Asp.net 2.0 C#?

I'm using ExcelLibrary to generate dynamic excel sheet. Which works perfectly.
Code
//create new xls file
string file = Server.MapPath("Discussion_Board_Report.xls");
Workbook workbook = new Workbook();
Worksheet worksheet = new Worksheet("Report");
worksheet.Cells[0, 0] = new Cell("COMM 226 Case Discussions Report");
worksheet.Cells[4, 0] = new Cell("Student ID");
worksheet.Cells[4, 1] = new Cell("User Name");
worksheet.Cells[4, 2] = new Cell("Case 1");
worksheet.Cells[4, 3] = new Cell("Case 2");
worksheet.Cells[4, 4] = new Cell("Case 3");
worksheet.Cells[4, 5] = new Cell("Topics");
worksheet.Cells[4, 6] = new Cell("Replies");
workbook.Worksheets.Add(worksheet);
workbook.Save(file);
Question
Problem with this code is, it's going to save the file in the server. I cannot save file in C://, because windows want allow me to save without user's permission! I want user to choose their own file path. How do I prompt a Save As dialog box?
It looks like you are using EPPlus, it has a Stream property on the ExcelPackage object that I believe you can use to write to the response stream. I've included some code that is similar to what I've used in the past to download a file.
I then put this logic into the page load of a page that I link to.
ExcelPackage package = new ExcelPackage();
ExcelWorksheet worksheet = package.Workbook.Worksheets.Add("Name Here");
///worksheet logic
var msArray = package.Stream;
var response = HttpContext.Current.Response;
response.Clear();
response.ClearHeaders();
response.ContentType = "application/vnd.ms-excel";
response.AddHeader("Content-Disposition", "attachment; filename={0}.xls".FormatWith(reportName));
response.AddHeader("Content-Length", msArray.Length.ToString());
response.OutputStream.Write(msArray, 0, msArray.Length);
response.Flush();
response.End();
What you need to do is stream the file to the user. I am sure the workbook class offers to save to a Stream, on this case the Response Stream
Something like this:
Response.ContentType = "application/vnd.ms-excel";
//Force the browser to offer to download the file
Response.AddHeader("content-disposition", "attachment; filename=file.xlsx");
workbook.SaveToStream(Response.OutputStream);
Update
Apparently, the author of the Excel library did not have support for this until .NET 4 and now claims to support it by doing this:
MemoryStream m = new MemoryStream(); // Temp Stream
workbook.Save(m);
m.CopyTo(Response.OutputStream);
Update 2
Actually, the author is not the one that claims that calling Workbook's Save method on a MemoryStream will work but rather someone else. The author actually recommends this silly piece of code.
DataSet ds = GetData();
MemoryStream m = new MemoryStream();
ExcelLibrary.DataSetHelper.CreateWorkbook(m , ds);
m.WriteTo(Response.OutputStream);
If that doesn't work, then I am afraid you are better off if you stop using that library completely and use EPPlus instead.
You can't do this in a pure application. Your code is running on the server - not on the user's machine.
You could generate Excel files on the server and then provide a link for your users to download them, but there's no way of getting the code on the server to save directly to your user's machine.
You could try doing something similar with a client-side plugin like Silverlight and you'll have limited access to the file system exposed by Silverlight.
Maybe you could let us know a bit more about what you're trying to do and we could provide better guidance.
You can try the FileUpload server Control.
In the yourfile.aspx use this:
<asp:FileUpload ID="FileUpload1" runat="server" />
In the yourfile.aspx.cs use this for keep the path that the use choose:
Server.MapPath(FileUpload1.FileName);

How to download a file from my webserver to my desktop?

I have looked around the internet for 3 hours now looking for a solution to my problem and I'm starting to wonder if anyone else has ever had this exact problem?
I am using IIS7 to host a website that does some local things around our office. Well, developing the site everything worked fine, but now that I am hosting the site I cannot (and neither can anyone else for that matter) click on the link to download the file that is needed. (lets just pretend they click on a link to download some random file from the webserver)
Well, it fails downloading the file from what I can guess as being a permissions error. I have looked for a solution but cannot seem to find one. I know very little about IIS7 so I do not understand very much about the Application Pool Identity stuff, although I did manage to grant full access for that identity to the file/folders. Anyways, here is the specific area it is messing up on..
This is a piece of the default.cshtml page that calls a Function from a .cs file:
//well.. pretty close to the exact thing, just got rid of a bunch of unecessary junk
#{
string tmp = Functions.downloadFile(fileName)
}
<html>
tmp
</html>
This is the part of the .cs file that actually downloads the file to the desktop
public static string downloadFile(string fileName) //i know this example doesnt
//use filename, but the original code does.
{
if (Directory.Exists("C:\WebSite\thingsToDownload"))
{
string[] files = Directory.GetFiles("C:\WebSite\thingsToDownload");
foreach (string s in files)
{
string[] tmp = s.Split('\\');
try
{
File.Copy(s,
Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
+ "\\" + tmp[tmp.Length - 1]);
}
catch (Exception e)
{
return "ERROR COPYING: " + e.Message;
}
}
return "GOOD";
}
return "DIRECTORY DOESNT EXIST";
}
After that is all said and done, i get "ERROR COPYING: Access to the path '\fileName' is denied.
My Guess is that the webserver does not have access to the person's desktop in order to put the files there.
If anyone can shed some light and help me get this it would be greatly appreciated!
If you want to download a file by clicking a link you should use response.
Here's a sample you can use for example when a user clicks a link or button or something:
const string fName = #"C:\picture.bmp";
FileInfo fi = new FileInfo(fName);
long sz = fi.Length;
Response.ClearContent();
Response.ContentType = Path.GetExtension(fName);
Response.AddHeader("Content-Disposition", string.Format("attachment; filename = {0}",System.IO.Path.GetFileName(fName)));
Response.AddHeader("Content-Length", sz.ToString("F0"));
Response.TransmitFile(fName);
Response.End();
It worked when you developed it, because it was running on your computer and thus had access to your desktop. There is no such thing as download to desktop. You can serve files and let the user decide where to save the file; on the desktop or elsewhere. But you have to do this over http, not directly from the server.

Categories