[ASP.NET][C#]WebForm FileUpload控制項上傳問題

幾年前軟體弱點檢測報告出爐,因此負責的網站關閉了幾個透過網頁上傳檔案的功能,最近其他部門的專案同意了網站檔案上傳,準備上線前,同事B通報了一個小問題:第一次進入頁面時操作上傳沒辦法上傳成功,第二次卻可以成功?

 

初步線索是網站使用了MasterPage + UpdatePanel +  + Panel 。

在msdn找到了UpdatePanel與FileUpload不相容的關鍵證詞,

 

文章後面提到了解題方法: 將送出檔案上傳的控制項(上傳按鈕)設為UpdatePanel的trigger項目,意思就是要一起postback!

 

筆記各種情境下的FileUpload: 
 

對照組1(無MasterPage、無UpdatePane):

Upload1.aspx

<body>
    <form id="form1" runat="server">
    <div>
        <asp:FileUpload ID="FileUpload1" runat="server" />
        <asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
    </div>
    </form>
</body>

Upload1.aspx.cs

protected void Page_Load(object sender, EventArgs e)
{

}

protected void Button1_Click(object sender, EventArgs e)
{
    if (FileUpload1.HasFile)
    {
        Response.Write(string.Format("對照組1:收到檔案,檔案名稱是{0}", FileUpload1.FileName));
    }
}

執行結果: 第一次就正常上傳!! simple is the best

透過HTML Form Data傳送檔案,Content type需要被設定為multipart/form-data。通常ASP.NET產生網頁到前端時都會自己設定好。

從Http header發現Content type 果然是 multipart/form-data,不做任何編碼將檔案上傳。

 

對照組2(無MasterPage、有UpdatePane):

Upload2.aspx

<body>
    <form id="form1" runat="server">
        <div>
            <asp:ScriptManager ID="ScriptManager1" runat="server"></asp:ScriptManager>
            <asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
                <ContentTemplate>
                    <asp:FileUpload ID="FileUpload1" runat="server" />
                    <asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
                </ContentTemplate>
            </asp:UpdatePanel>
        </div>
    </form>
</body>

Upload2.aspx.cs

protected void Page_Load(object sender, EventArgs e)
{
    
}

protected void Button1_Click(object sender, EventArgs e)
{
    if (FileUpload1.HasFile)
    {
        Response.Write(string.Format("對照組2:收到檔案,檔案名稱是{0}", FileUpload1.FileName));
    }
}

按下按鈕後,卻沒有收到檔案資訊,透過偵錯FileUpload.HasFile是False

依照msdn的建議,我們透過server side程式方式將上傳按鈕button1加入trigger

protected void Page_Load(object sender, EventArgs e)
{
    UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = Button1.UniqueID });
}

果然成功上船了!

如果有加上trigger,Header中的content type = multipart/form-data

如果沒加上trigger,content type= application/x-www-form-urlencoded(預設值)

 

實驗組1:有MasterPage、有UpdatePane

情境很貼近網站的現狀。

先新增一個主版頁面

<body>
    <form id="form1" runat="server">
        <div>
            <asp:ScriptManager ID="ScriptManager1" runat="server"></asp:ScriptManager>
            <asp:Label ID="Label1" runat="server" Text="我是主版頁面(MasterPage)"></asp:Label>
            <br />
            <asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
                <ContentTemplate>
                    <asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
                    </asp:ContentPlaceHolder>
                </ContentTemplate>
            </asp:UpdatePanel>
        </div>
    </form>
</body>

接著新增一個使用主版頁面的Web Form表單

Upload3.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Upload/Test.Master" AutoEventWireup="true" CodeBehind="Upload3.aspx.cs" Inherits="WebApplication1.Upload.Upload3" %>
<%@ MasterType VirtualPath="~/Upload/Test.Master" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
    <asp:FileUpload ID="FileUpload1" runat="server" />
    <asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
</asp:Content>

Upload3.aspx.cs

也類似對照組2的作法Update Panel Trigger 加入整個頁面postback的觸發控制項button1

protected void Page_Load(object sender, EventArgs e)
{
    Master.UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = Button1.UniqueID });
}
protected void Button1_Click(object sender, EventArgs e)
{
    if (FileUpload1.HasFile)
    {
        Response.Write(string.Format("實驗組1:收到檔案,檔案名稱是{0}", FileUpload1.FileName));
    }
}

第一次就成功了!

疑,怎麼沒發生同事B通報的事件,案情不單純!

header中的content type = multipart/form-data

 

經過比對差異發現: 實際的程式FileUpload控制項放在一個初始visible=False的Panel容器中,我們接著再加一個實驗2。

 

實驗組2:有MasterPage、有UpdatePane、有Panel(visible=false)

Upload4.aspx

預設PnlDetail Visible = false

<%@ Page Title="" Language="C#" MasterPageFile="~/Upload/Test.Master" AutoEventWireup="true" CodeBehind="Upload4.aspx.cs" Inherits="WebApplication1.Upload.Upload4" %>

<%@ MasterType VirtualPath="~/Upload/Test.Master" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
    <asp:Button ID="btnDetail" runat="server" Text="顯示上傳區域" OnClick="btnDetail_Click" />
    <asp:Panel ID="PnlDetail" runat="server" Visible="false">
        <asp:FileUpload ID="FileUpload1" runat="server" />
        <asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
    </asp:Panel>
</asp:Content>

Upload4.aspx.cs

protected void Page_Load(object sender, EventArgs e)
{
    Master.UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = Button1.UniqueID });
}
protected void Button1_Click(object sender, EventArgs e)
{
    if (FileUpload1.HasFile)
    {
        Response.Write(string.Format("實驗組2:收到檔案,檔案名稱是{0}", FileUpload1.FileName));
    }
}

protected void btnDetail_Click(object sender, EventArgs e)
{
    PnlDetail.Visible = true;
}

 

點選顯示上傳區域按鈕,瀏覽檔案上傳

出現了同事B通報的問題,postback時沒有一起把檔案傳送上來!!

果然Http header中的content type=application/x-www-form-urlencoded

 

Google搜尋後發現有些解答是在Page_Load加上這一行Page.Form.Attributes.Add("enctype", "multipart/form-data")

但似乎沒有效果!!

Upload4.aspx.cs

protected void Page_Load(object sender, EventArgs e)
{
    Master.UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = Button1.UniqueID });
    Page.Form.Attributes.Add("enctype", "multipart/form-data");
}

查看網頁原始檔,enctype="multipart/form-data"

但form post時,content type卻還是 application/x-www-form-urlencoded(預設值)

 

幾經波折,從stackoverflow得到一個可能原因,如果放置fileupload控制項的容器被設定visible=false,fileupload控制項就沒辦法render成html,間接影響form無法設定編碼種類為 enctype="multipart/form-data",最後造成fileupload控制項選取的檔案無法postback到sever。

解決的方向是想辦法在執行上傳前執行一次完整的postback。

 

解決方案1: 上傳前先執行一次MasterPage的任一按鈕作完整postback。

我們試著在Master Page增加一個按鈕full post back作為整頁PostBack。

<asp:Button ID="Button1" runat="server" Text="full post back" OnClick="Button1_Click" />
protected void Button1_Click(object sender, EventArgs e)
{

}

測試看看!按下full post back按鈕,再瀏覽檔案然後上傳。

果然成功上傳了!不過這個解法很阿丹!哈!換下一帖!

 

解決方案2: content page的Page Load事件中將Update Panel Trigger加入顯示上傳區域btnDetail按鈕控制項,也可以work!
這個假設顯示上傳區域btnDetail是上傳按鈕的前一個事件(打開上傳區域)!解法和解決方案1很類似,想辦法先執行一次full postback
Upload4.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
    Master.UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = Button1.UniqueID });
    Master.UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = btnDetail.UniqueID });
}

雖然也可以解決,但好像也不太適合!

 

解決方案3:透過ScriptManager註冊觸發postback的控制項Button1,同時加上編碼屬性multipart/form-data

Upload4.aspx.cs

protected void Page_Load(object sender, EventArgs e)
{
    Master.UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = Button1.UniqueID });
    Page.Form.Attributes.Add("enctype", "multipart/form-data");
    ScriptManager.GetCurrent(Page).RegisterPostBackControl(Button1);
}

3種解決方法都可以成功的在第一次執行就上傳成功,最後選擇了方案3,將上傳按鈕註冊到postback control中。

 

 

小結:

  • 關鍵: Update Panel Trigger + RegisterPostBackControl
  • 也許這就是Web Form的宿命,好用的UpdatePanel讓我們輕易局部更新,但總有點遺憾。
  • 常常為了趕案子沒時間深入了解背後解題的原因,技術債就這麼欠下來了。

 

版本資訊:

Visual Studio 2015

 

 

參考:

UpdatePanel控制項概觀

HP Fortify Often Misused: File Upload 允許使用者上傳檔案可能會使攻擊者在伺服器執行已注入的危險內容或惡意程式碼?

FileUpload and UpdatePanel: ScriptManager.RegisterPostBackControl works the second time

New Issue with FileUpload in UpdatePanel - Works After first full post back

ScriptManager.RegisterPostBackControl Method