WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

URL Rewrite 2.0 と日本語の問題

by WebSurfer 2013年12月2日 17:31

Microsoft が提供している IIS 7.x 用の URL Rewrite 2.0 モジュール を使用しての日本語の書き換えがうまくいかないという話です。

なお、この問題は globalization 要素の requestEncoding 属性を Shift_JIS に設定した場合に限られます。デフォルトの UTF-8 の場合は問題ありません。

URL Rewrite 2.0 による書き換え

例えば、以下のように、日本語を使用した URL を書き換えるケースを考えます。

http://host/dir/あ ⇒ http://host/default.aspx?n=あ

その場合、URL Rewrite 2.0 では URL 書き換えルールは以下のようになります。

<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="Japanese Query String Test">
          <match url="dir/(.+)$" />
          <action 
            type="Rewrite" 
            url="default.aspx?n={R:1}" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

上記で、{R:1} というのは正規表現の後方参照から取得される文字列で、パターン dir/(.+)$ ではカッコで囲った部分になります。例えば、"http://host/dir/xxx" という入力なら {R:1} に該当するのは "xxx" になります。(ちなみに、{R:0} は "dir/xxx" になります)

ブラウザが要求を出す際、先の記事 ブラウザによる URL のエンコーディング で書きましたように、URL を UTF-8 として URL エンコーディングします。

"あ" の UTF-8 でのバイト列は E3 81 82 ですので、"http://host/dir/あ" は "http://host/dir/%E3%81%82" にエンコーディングされてからサーバーに送信されます。

URL Rewrite モジュールでは、"http://host/dir/%E3%81%82" という入力から、正規表現パターン dir/(.+)$ の後方参照として "%E3%81%82" を取得し、それをデコーディングした文字列が {R:1} に渡される仕組みになっています。(ソースコードを見たわけではなく、検証結果での確認ですが)

問題はその際の Encoding の判定です。

requestEncoding 属性が UTF-8(デフォルト)であれば、使用されている Encoding は UTF-8 と判定され、書き換え後の URL は正しく "default.aspx?n=あ" となります。

しかし、requestEncoding 属性が Shift_JIS に設定されていると、"%E3%81%82" の Encoding は Shift_JIS と判定され、書き換え後の URL は "default.aspx.aspx?n=縺・" とクエリ文字列の部分が文字化けしてしまいます。("縺・" は Shift_JIS のバイト列で E3 81 81 45 になります)

requestEncoding 属性が Shift_JIS でこの問題を避ける方法はあるでしょうか?

試しに、"http://host/dir/あ" で "あ" の部分を Shift_JIS の URL エンコード、即ち "http://host/dir/%82%A0" としてブラウザのアドレスバーに入力してみました。

その結果が一番上の画像なのですが、結果は同じように文字化けしてしまい、やっぱりダメでした。

ブラウザからサーバーに送信される URL は "http://host/dir/%82%A0" となりますが(Fiddler2 で確認)、サーバーが受信して URL Rewrite モジュールに渡す時に "%82%A0" が "%E3%81%82" になってしまうようです。(上の画像の X-Original-URL 参照)

というわけで、Microsoft の IIS 7.x 用 URL Rewrite 2.0 モジュールを使う際は、requestEncoding 属性を UTF-8(デフォルト)に限定した方がよさそうです。

UTF-8 なら "http://host/dir/%82%A0" という Shift_JIS で URL エンコーディングされた URL も正しく "default.aspx?n=あ" に書き換えられます。

参考までに検証に使った aspx ページ(0024-QueryString.aspx)を以下に載せておきます。

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
    
  protected void Page_Load(object sender, EventArgs e)
  {
    Encoding enc = Request.ContentEncoding;
        
    Label1.Text = "Request.ContentEncoding.EncodingName: " + 
        enc.EncodingName;
              
    Label2.Text = "X-Original-URL: ";
    string[] values = 
        Request.Headers.GetValues("X-Original-URL");
    if (values != null)
    {
      string url = values[0];
      Label2.Text = Label2.Text + url;
      Label2.Text = Label2.Text + " (Url-decoded: " +
            HttpUtility.UrlDecode(url, enc) + ")";
    }

    Label3.Text = "Request.RawUrl: " + Request.RawUrl;
    Label4.Text = "Request.Url.OriginalString: " + 
        Request.Url.OriginalString;

    string queryString = Request.QueryString["n"];
    Label5.Text = 
        "Request.QueryString[\"n\"]: " + queryString;
                
    if (queryString != null)
    {
      string s = " (byte array: ";
      byte[] b = enc.GetBytes(queryString);
      for (int i = 0; i < b.Length; i++)
      {
        s = s + String.Format("[{0:X2}]", b[i]);
      }
      Label5.Text = Label5.Text + s + ")";
    }
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    <asp:Label ID="Label1" runat="server"></asp:Label>
    <br />
    <asp:Label ID="Label2" runat="server"></asp:Label>
    <br />
    <asp:Label ID="Label3" runat="server"></asp:Label>
    <br />
    <asp:Label ID="Label4" runat="server"></asp:Label>
    <br />
    <asp:Label ID="Label5" runat="server"></asp:Label>
  </div>
  </form>
</body>
</html>

Tags: ,

Windows Server

また reCaptcha が動かなくなりました

by WebSurfer 2013年11月23日 15:13

ブログのスパムコメント防止のため、BlogEngine.NET には reCAPTCHA というツールが実装されていますが、今日、これが動かなくなっていることに気がつきました。

reCAPTCHA のスクリプトエラー

reCAPTCHA に関係するコードは一切いじってないのに、上の画像のように Recaptcha が定義されてないというスクリプトエラーが出ます。

この Recaptcha の定義は、Google のサーバーからダウンロードする recaptcha_ajax.js という外部スクリプトファイルに含まれています。

このスクリプトファイルの URL を直接ブラウザのアドレスバーに入力して要求をかけてみると HTTP 404(見つからない)エラーになります。

最初は Google が reCAPTCHA サービスをやめたのかと思いましたが、そうではなくてスクリプトファイルを提供するサーバーの URL が変わったのが原因でした。

変更前: http://api.recaptcha.net/js/recaptcha_ajax.js
変更後: http://www.google.com/recaptcha/api/js/recaptcha_ajax.js

ユーザー登録してあるのに Google から予告はなかったと思うのですが・・・ Google のサイトの Displaying reCAPTCHA Without Plugins を見ると確かに URL は変更後のものになってますが、後出しなんじゃないかと・・・

11 月の初旬にはこの問題が stachoverflow などに報告されていますので、自分は 2 週間ぐらい問題に気がつかなかったようです。(汗)

実は、ネットサーフィン時に不出��なサイト(?)で多発するスクリプトエラーがわずらわしいので IE のオプション設定で[スクリプトのデバッグを使用しない]にチェックを入れてました。そうすると上の画像のようなエラーメッセージは出ませんから。

原因が分かれば修正するのは簡単ですので早速対応しました。

この URL は以下のファイルの RecaptchaControl という名前の Web カスタムコントロールの中にハードコーディングされてます。

\App_Code\Extensions\Recaptcha\RecaptchaControl.cs

URL を web.config の AppSettings に設定するのがよさそうだとは一瞬思いましたが、面倒なのでハードコーディングした部分のみを修正して解決しました。(笑)

Tags:

BlogEngine.NET

GridView 上の DropDownList に ToolTip

by WebSurfer 2013年11月2日 18:33
注意:
以下の記事の例では、Products テーブルの CategoryID が NULL の場合、および更新の際に NULL を入力する場合の対応は考えていません。NULL 対応は別の記事 DropDownList での NULL の処置 を見てください。

GridView などで更新操作を行う際、DropDownList を利用してユーザー入力に便宜を図ることがあります。その際、ツールチップを利用して DropDownList 上の各項目の説明を表示するという話です。

GridView 上の DropDownList に ToolTip 表示

ここで紹介する例には Microsoft が提供している Northwind サンプルデータベースの Products テーブルと Categories テーブルを使用しています。

Products テーブルの中の ProductName, CategoryID フィールドを GridView 上で更新する際、CategoryID の列に DropDownList を表示するようにします。

DropDownList には、ユーザーが見ても何だか分からない ID (CategoryID) を表示するのでははなくて、ユーザーが読んで理解できる名前 (CategoryName) を表示します。

さらに DropDownList を開いて項目一覧を表示し、それにマウスカーソルを当てると各項目の説明 (Description) をツールチップに表示するようにします。上の画像を見てください。

DropDownList が閉じている時に DropDownList にマウスカーソルを当てると、選択された項目の説明がツールチップに表示されるようにしています。

上の画像を表示したサンプルコードは以下の通りです。詳細はコメントに説明を書きましたので、それを参考にしてください。手抜きでスミマセン。(汗)

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

  // DropDownList には RowDataBound や ItemDataBound に該当す
  // るイベントがないので、各項目にツールチップを追加するには
  // 以下のような方法を取らざるを得ない。
  // 
  // GridView の[編集]ボタンがクリックされるとポストバック
  // され、当該行内に DropDownList がレンダリングされる。その
  // 際に DropDownList.DataBound イベントが発生するので、そこ
  // で DropDownList およびその中の ListItem にツールチップを
  // 設定する。
  protected void DropDownList1_DataBound(object sender, 
        EventArgs e)
  {
      DropDownList ddl = (DropDownList)sender;

      // 再度 DB にクエリを投げてデータを取得。(他に方法が
      // 見つからない)
      DataView dv = (DataView)SqlDataSource2.
            Select(DataSourceSelectArguments.Empty);

      foreach (ListItem item in ddl.Items)
      {
          dv.RowFilter = 
                String.Format("CategoryID='{0}'", item.Value);
          string description = (string)dv[0]["Description"];
            
          // ListItem には ToolTip プロパティはないので、直接
          // title 属性に description を設定する。
          item.Attributes["title"] = description;

          // DropDownList の ToolTip には、選択されている項目の
          // description を設定する。
          if (item.Selected)
          {
              ddl.ToolTip = description;
          }
      }

      // ユーザーが DropDownList 中の項目をクリックして選択され
      // ている項目を変更した場合、それに応じて DropDownList の
      // ToolTip を書き換えるためのスクリプトを追加        
      string csname1 = "jQuery1.8.3";
      string csurl = "~/Scripts/jquery-1.8.3.js";
      string csname2 = "ChangeDropDownListToolTipScript";
      Type cstype = this.GetType();
      ClientScriptManager cs = Page.ClientScript;

      // jQuery を利用するので参照を追加。
      if (!cs.IsClientScriptIncludeRegistered(cstype, csname1))
      {
          cs.RegisterClientScriptInclude(
                cstype, csname1, ResolveClientUrl(csurl));
      }

      // DropDownList の ToolTip 書き換え用スクリプト追加。
      if (!cs.IsClientScriptBlockRegistered(cstype, csname2))
      {
          StringBuilder cstext = new StringBuilder();
          cstext.Append("$(function () {");
          cstext.Append("$('#" + ddl.ClientID + "').change(");
          cstext.Append("function () {");
          cstext.Append("$(this).attr('title', $('#" +
                ddl.ClientID +
                " option:selected').attr('title'));");
          cstext.Append("});");
          cstext.Append("});");
          cs.RegisterClientScriptBlock(
              cstype, csname2, cstext.ToString(), true);
      }
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
  <title></title>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    <asp:SqlDataSource ID="SqlDataSource1" 
      runat="server" 
      ConnectionString="<%$ ConnectionStrings:Northwind %>" 
      SelectCommand=
        "SELECT TOP 10 
          p.[ProductID], p.[ProductName], 
          p.[CategoryID], c.[CategoryName] 
        FROM [Products] AS p 
        INNER JOIN [Categories] AS c 
        ON p.[CategoryID] = c.[CategoryID] 
        ORDER BY p.[ProductID]" 
      UpdateCommand=
        "UPDATE [Products] 
        SET [ProductName] = @ProductName, 
            [CategoryID] = @CategoryID 
        WHERE [ProductID] = @ProductID">
      <UpdateParameters>
        <asp:Parameter Name="ProductID" Type="Int32" />
        <asp:Parameter Name="ProductName" Type="String" />
        <asp:Parameter Name="CategoryID" Type="Int32" />                
      </UpdateParameters>
    </asp:SqlDataSource>

    <asp:SqlDataSource ID="SqlDataSource2" 
      runat="server" 
      ConnectionString="<%$ ConnectionStrings:Northwind %>" 
      SelectCommand=
        "SELECT [CategoryID], [CategoryName], [Description] 
        FROM [Categories]">
    </asp:SqlDataSource>
        
    <asp:GridView ID="GridView1" 
      runat="server" 
      AutoGenerateColumns="False" 
      DataKeyNames="ProductID" 
      DataSourceID="SqlDataSource1">
      <Columns>
        <asp:CommandField ShowEditButton="True" />
        <asp:BoundField DataField="ProductID" 
          HeaderText="ID" 
          InsertVisible="False" 
          ReadOnly="True" 
          SortExpression="ProductID" />
        <asp:BoundField DataField="ProductName" 
          HeaderText="Product Name" 
          SortExpression="ProductName" />
        <asp:TemplateField HeaderText="Category" 
          SortExpression="CategoryName">
          <EditItemTemplate>                        
            <asp:DropDownList ID="DropDownList1" 
              runat="server" 
              DataSourceID="SqlDataSource2" 
              DataTextField="CategoryName" 
              DataValueField="CategoryID" 
              SelectedValue='<%# Bind("CategoryID") %>' 
              OnDataBound="DropDownList1_DataBound">
            </asp:DropDownList>
          </EditItemTemplate>
          <ItemTemplate>
            <asp:Label ID="Label1" 
              runat="server" 
              Text='<%# Bind("CategoryName") %>'>
            </asp:Label>
          </ItemTemplate>
        </asp:TemplateField>
      </Columns>
    </asp:GridView>
  </div>
  </form>
</body>
</html>

Tags: , ,

ASP.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar