經驗交流

處理含 ASCII 92 字元之文字輸入值

問題說明
 如果您的 MySQL character set 採用「big5」的話,它本身能辨識雙位元文字,那麼以下的內容是您必須要注意的。
 BIG5 碼系統為兩位元組之內碼系統,共可定義 19782 個字碼。其高、低位元組的範圍如下:
 高位元組:0x81 ∼ 0xFE(ASCII 129 ∼ 254)
 低位元組:0x40 ∼ 0x7E 與 0xA1 ∼ 0xFE(ASCII 64 ∼ 126 與 161 ∼ 254)
 在許多程式語言之中,ASCII 92(\)被當作是跳脫(escape)字元,在程式中需要輸出特定字元時,先加上 \,才能被系統所辨識出來。例如:在指定字串變數值時,$Str = "PHP 是一種程式語言",前後都會用到 " 這個字元,如果希望在字串中使用 " 的話,則可以這麼寫:$Str = "\"PHP\" 是一種程式語言"。如此一來,「PHP」前後的雙引號就可以被視為字串的一部份了。
 問題就出在這裡!假如使用者在文字框中輸入「成功」二字,我們來看看傳到伺服器端的資料是什麼(以下一併顯示中文與十六進位的 ASCII 碼):
 使用者端傳送:成(A6 A8)功(A5 5C
 伺服器端接收:成(A6 A8)功(A5 5C 5C
 有什麼不同?「功」字的後半部是 5C,轉成十進位是 92,剛好就是上述的跳脫字元。為了正確地傳送此一字串,PHP 會自動在該字元之前多加個 \。
 顯示在畫面上的是「成功\」,只是有點礙眼,沒啥影響。如果要存入資料庫的話,問題就來了:
 $SQL = "INSERT INTO mytable VALUES ('$Str');";
 對資料庫而言,它將收到一個這樣的命令:
 INSERT INTO mytable VALUES ('成功\');
 原本「成功」前後的單引號是用來標示字串的,但後面那個單引號加上 \ 之後,就被視為字串的一部份,而該命令就少了一個單引號了。不正確的命令,當然不能期待它會產生正確的執行結果。

我的做法
 只要可供使用者輸入文字的元件(如 Text、Textarea...),都需要經過下列函數的過濾,才能組成對資料庫執行存取動作的 SQL 敘述句。
01    function Fix_Backslash($org_str) {
02      if ( mysql_client_encoding() != "big5" ) return $org_str;

03      $tmp_length = strlen($org_str);

04      for ( $tmp_i=0; $tmp_i<$tmp_length; $tmp_i++ ) {
05        $ascii_str_a = substr($org_str, $tmp_i , 1);
06        $ascii_str_b = substr($org_str, $tmp_i+1, 1);

07        $ascii_value_a = ord($ascii_str_a);
08        $ascii_value_b = ord($ascii_str_b);

09        if ( $ascii_value_a > 128 ) {
10          if ( $ascii_value_b == 92 ) {
11            $org_str = substr($org_str, 0, $tmp_i+2) . substr($org_str,$tmp_i+3);
12            $tmp_length = strlen($org_str);
13          }
14          $tmp_i++;
15        }
16      }

17      $tmp_length = strlen($org_str);
18      if ( substr($org_str, ($tmp_length-1), 1) == "\\" ) $org_str .= chr(32);

19      $org_str = str_replace("\\0", "\ 0", $org_str);
20      return $org_str;
21    }
 02 如果您的 MySQL character set 是 big5 之類的,它本身能辨識雙位元文字時,才需要本函數來加以處理。否則,就可以直接回傳原字串了。
 請注意:這裡使用 mysql_client_encoding 函數來判斷您的 MySQL character set,而此函數需在 PHP 4.3.0 版以後才能支援。
 03 先計算整個字串的總長度,以便後續的迴圈執行「逐字元檢查」的動作。
 05 - 06 依序挑出每個字元,與它的下一個字元,意即連續擷取兩個字元。
 07 - 08 將挑出的兩個字元分別轉成 ASCII 碼。
 若所得的第一個字元大於 ASCII 128 的話(可能是中文字),執行 10 - 13 之間的程式;否則,將迴圈的指標多加 1,這個中文字(兩個字元)算是過關了。
 所得的第二個字元恰好是 ASCII 92 的話(如:許、功、俞、餐等字),11 可以將被多加上去的 \ 移除。假如被檢查的字串是「成功\了」,請看做法:
 檢查到「功」時 $tmp_i 是 2,substr($org_str, 0, $tmp_i+2) 可以擷取「成功」二字,再用 substr($org_str, $tmp_i+3) 擷取「了」字,並組合起來就行了。
 12 重算整個字串的總長度,因為字串長度改變了。
 17 重算整個字串的總長度,供第 18 行使用。
 18 在上述的處理程序之後,如果在字串末尾還有 \ 的話,在其後加個空白。
 由於前後文字之間可能組合出 \0 字元(Null),它也會干擾程式的正常運作,所在第 19 行利用 str_replace 函數,將所有的 \0 改成 \ 0(中間加個空白)。
 20 大功告成了。
2005.2.18 修訂
經驗交流