How should password information be stored?

Plain text is NEVER an option

In last Christmas (2011), there was a breaking news (reported: 1, 2, 3) that over 6 million online user credentials were leaked out from CSDN, a popular China online community for programmers that stores user credentials in plant text, resulting in 50 million user accounts (users use the same credential for multiple websites, including social networks and personal emails) put under security risk.

As we never know when and how hackers steal the user records in the database, storing user credential information in plain text format in the database is never an option for any reasons.

Encryption of password

As a good practice, the user credentials should be encrypted to write to the database so that even the database records are exposed by hackers, the user credential is still less risky. There is many ways to encrypt password, namely using a hash function, adding static salt,  adding dynamic information.

Hashing

Hashing a password is the way to convert the password from a human-recognizable strings (e.g. “QuoKa88$%”) into a set of non-recognizable strings (e.g. “398db0fdd8a26857080a77fd4996377d”) so that hackers who steals the user credential cannot recognize the actual password string.

Hashing the password is the very first and the most basic step to store password information. Commonly used hash functions in PHP include sha1(), md5() and hash().

<?php

$password_hashed = md5($password);

?>

However, it’s clearly stated in the PHP documentation that solely using these functions to secure password is not recommended as they can be easily hacked by modem computer’s computation abilities.

Adding salt

One of the attack that breaks the hashed password is called “Dictionary attacks“. Generally users tend to adopt meaningful words as the password to that they can easily remember it. Hackers collect million pairs of meaningful words and their hashed string to build a “hash dictionary”. So when they obtain a hashed password, they can look up the “hash dictionary” and find out the original password.

Adding salt to the hashed password will make the password more secure against the dictionary attack. Salt is additional strings, generated randomly, added to the password  so that the password will no likely exists in the “hash dictionary”.

There are two ways to add salt to a password. The first is to add the salt to the original password so as to make the original password longer in length (increase the password complexity) and less likely to be found in an ordinary dictionary. The second is to add the salt to the hashed password so as to make the “hash dictionary” malfunction.

<?php
$salt_before_hash = "eGj1&E2%k@";

$salt_after_hash = "12fd53a3b";

$password_hashed = md5($salt_before_hash.$password)  . $salt_after_hash;
?>

Adding dynamic information

Adding salt to password, either before or after the hashing, has one potential vulnerability : the salt is a static string (no matter how complicated it is) that the hacker can easily identify the salt by comparing the multiple hashed passwords. Once the table of user credentials is being stolen, hackers only need a few days (if not a few hours) to break the salts.

To fix this vulnerability, we could use dynamic information (such as date of account created, record row ID, user ID, check sum of the password itself etc.) which is static to one user but dynamic to other users.

<?php

$salt_before_hash =$userID . $date_user_account_created;

$salt_after_hash = dechex(crc32($password));

$password_hashed =md5($salt_before_hash.$password)  . $salt_after_hash);

?>

Adding interference

Finally we can add interference in the whole process to make further secure the password encryption. Commonly used interference include re-odering the sequence of the strings and cut the hashed password to a long-enough length.

<?php

$salt_before_hash = substr($userID . $date_user_account_created, 5,10);

$salt_after_hash = dechex(crc32($password));

$password_hashed =strrev(substr(md5($salt_before_hash.$password))  . $salt_after_hash, -25));

?>

固定 Dataram RAMDisk 的磁碟代號 – 附示範程式

[Note: We have an English version for this post. Please read here]

背景

自從我編寫了一篇如何避免使用映像檔也可以固定 Dataram RAMDisk 磁碟代號的文章之後,就收到許多讀者的查詢。當中許多涉及執行上的細節,但由於該篇文章並沒有提供一個可直接執行的示例程式,故此在回應讀者的查詢時難免出現描述上的困難。現在,我再撰寫此文及提供一個可以直接執行的示例程式,希望能方便讀者使用和研究。

這篇文章並不是要取代之前所撰寫的文章,而是作為實作的補充。如你對示例程式的原理和解說有興趣,我強烈建議您去閱讀之前撰寫的文章

在本文中,我提供了兩個示例程式:

  1. StartUp.bat:在電腦啟動時,將 RAMDisk 的磁碟代號進行修正,並將一些常用程式自動載入 RAMDisk 中,以及將這些瀏覽器的 Cache 和下載文件設定在 RAMDisk 中,從而達致最佳的瀏覽器速度。
  2. ShutDown.bat:在電腦關機時,將 RAMDisk 的瀏覽器設定檔案及已下載的文件複製到硬碟機中,以免關機後失去這些設定和文件。

這兩個程式是按照我自己的電腦設定來運作的,故此如果你要在你的電腦中使用這些程式,你必須根據以下條件來設定你的電腦。當然你也可以直接修改程式碼去符合你的電腦設定,但前提是你必須了解這些程式碼的運作原理。(詳見此文

電腦設定

請確保你的電腦符合以下設定/假設:

  1. RAMDisk 的磁碟標籤(Disk label)必須是 “RAMDISK” (全部大寫)。設定步驟見原文第 2 步。
  2. RAMDisk 的磁碟代號將會固定為 R: .
  3. 電腦開機後,我們希望電腦能夠自動將  C:\MyRAMDiskFiles 資料夾中的檔案複製到 RAMDisk之中。這個資料夾主要是放置一些常用程式,使他們能夠直接在 RAMDisk 上執行,加快速度。舉個例子,我希望將 Firefox 放在 RAMDisk 上執行,那麼我就先將 Firefox 的程式檔案放在 C:\MyRAMDiskFiles\Programs\Firefox\,那麼當 StartUp.bat 執行後,便可以直接點擊 R:\Programs\Firefox\Firefox.exe 去執行 Firefox 了。
  4. 我的 Google Chrome 安裝在 C:\Program Files,我希望 Chrome 將瀏覽互聯網時產生的暫存檔 (Cache)儲存在 RAMDisk 中,但是由於 Chrome 的暫存檔是放在 Chrome 的安裝目錄下,所以 StartUp.bat 會將該暫存檔目錄轉化為一個虛擬連接,直接連接到 R:\TEMP\BrowserCache 目錄中。如此一來,所有儲存到暫存檔目錄的檔案都會被實際儲存在 RAMDisk 之中,減少硬碟讀寫,並且會在關機時自動清除這些暫存檔。
  5. 在使用電腦的過程中, RAMDisk 上的程式設定可能會被修改(例如 Firefox 插件更新),我希望這些設定在關機後仍能夠保留。所以,在電腦關機前,ShutDown.bat 會將這些程式檔案複寫到硬碟 C:\MyRAMDiskFiles 
  6. 此外,所有從互聯網上下載的檔案,都會被儲存到 RAMDisk 之中 (R:\Download), 並且在電腦關機時,將這些檔案複製到硬碟  C:\Download 資料夾中。

開始

第 1 步

用記事本建立兩個程式檔案: StartUp.bat 和 ShutDown.bat, 並儲存在 C:\ (或其他你喜歡的資料夾中,在之後的步驟中你需要指定這些程式檔案的位置。)

注意!!我們的程式碼是以 DOS Batch Script 形式編寫, “斷行”(line-break) 在此 DOS Batch Script 中有特定意義,故千萬不要隨便在程式碼中增加“斷行”。如果你使用“複製-貼上”的方式從本文建立程式碼,我們建議你小心檢查一下有沒有額外的“斷行”被加插入去。為避免麻煩,我們建議你直接下載這些程式碼:  RAMDisk fix drive letter - sample scripts

此外,你必須將程式碼中的 [Your-Windows-user-name] 修改為你的 Windows 用戶名稱。

程式 1: C:\StartUp.bat

@echo off

@rem assign the key variables, see assumption #1 & #2
SET _label=RAMDISK
SET _ramdrive=R:

@rem Find the drive letter which has volume name set as RAMDISK, and mount it to _ramdrive
FOR /F "skip=1 tokens=1 delims=: " %%a IN ('wmic logicaldisk where "VOLUMENAME='%_label%'" GET CAPTION') DO (
	subst %_ramdrive% %%a:\
)

@rem initialize the ramdisk - automatically copy the files in C:\MyRamDriveFiles to the ramdisk after the Windows starts. see assumption #3
mkdir %_ramdrive%\Programs
xcopy C:\MyRamDriveFiles\*.* %_ramdrive%\Programs\ /E/Q/Y/R

@rem link the google chrome cache folder to ramdisk. Remeber to replace [Your-Windows-user-name] with your own, see assumption #4
mkdir %_ramdrive%\TEMP\BrowserCache
mklink /j "C:\Users\[Your-Windows-user-name]\AppData\Local\Google\Chrome\User Data\Default\Cache" "%_ramdrive%\TEMP\BrowserCache"

@rem create a dir in ramdisk to stores the downloaded files, you will need to set up Firefox to store downloaded files in R:\Download, see assumption #6
mkdir %_ramdrive%\Download

@echo on

程式 2: C:\ShutDown.bat

@echo off

@rem see assumption #2
SET _ramdrive=R:

@rem copy all program files back to harddisk -  see assumption #5
xcopy %_ramdrive%\Programs\*.* C:\MyRamDriveFiles\ /Q/Y/R/S/D/C

@rem copy all downloaded files back to harddisk  -  see assumption #6
mkdir C:\Download
xcopy %RAMDISK%\Download\*.* C:\Download\ /S/D/Q/R/C

rem disconnect the google chrome cache folder linking -  see assumption #4
rmdir "C:\Users[Your-Windows-user-name]\AppData\Local\Google\Chrome\User Data\Default\Cache"

rem delete the drive mounting
subst %RAMDISK% /D

echo on

第 2 步

參照此文並執行該文的步驟3.

第 3 步

現在我們已經利用本機群組原則編輯器(Group Policy Editor)在 StartUp.bat 加入了“啟動” (Startup)項目中,請重複該文的步驟 3.2, 將 ShutDown.bat 加入到 “關機” (Shutdown) 項目中。

至此,我們應該在 “啟動”(Startup)中已經有 “C:\StartUp.bat”, 而在 “關機”(Shutdown)中則有”C:\ShutDown.bat” 項目.

Startup script

第 4 部

請確保你的電腦符合上述的“電腦設定”的要求,特別檢查一下你的電腦中已經建立了 “C:\MyRAMDiskFiles” 和 “C:\Download” 資料夾。一切妥當後,請重啟電腦即可。

如何固定 Dataram RAMDisk (虛擬磁碟) 的磁碟代號

[Note: We have an English version for this post. Please read here]

RAMDisk

Dataram RAMDisk 是一個非常流行的免費 RAMDisk (虛擬磁碟,即將一部分記憶體分隔出來當成普通磁碟機使用。) 軟件。它與特色包括免費,最高可以設定 4GB 容量的虛擬磁碟(足夠應付大部分用途,收費版本可設定 4GB 以上的容量),免費版還同時支持 32 Bit 和 64Bit (某些競爭對手只有 32Bit 是免費,64Bit要收費),而且在各項測試中性能表現甚佳。如果您使用的是 32 Bit的 Windows 系統的話,Dataram RAMDisk 還可以突破系統4GB 記憶體的限制,讓您將 4GB 以外的記憶體設定為虛擬磁碟,不致浪費了寶貴的記憶體資源。我自己則從升級到 Win7 系統開始就使用 Dataram RAMDisk 至今已經有好長時間,對於它的性能和穩定性完全沒有任何問題。

唯獨…

我對這個軟件的唯一投訴就是,軟件本身無法將虛擬磁碟的磁碟代號進行固定,而是每次啟動時按最後的磁碟代號自動設置,這就為用家帶來很大的不便了。舉個例子,今天我開啟電腦後,我的虛擬磁碟是 K:,我將我的 Firefox 緩存檔設定在 K:\Cache 目錄下。第二天,我在啟動電腦前插上了一個 USB 磁碟,電腦開啟後,會自動將這個USB磁碟設定為 K: ,然後虛擬磁碟就順序使用了 L: 作為磁碟代號。這下可好了,當我打開 Firefox 的時候,它會將所有緩存資料儲存在 K:\Cache,也就是儲存在我的 USB 磁碟中,而不是虛擬磁碟中。當然,沒有一個用戶希望發生這樣的事情。用戶需要一個固定不變的磁碟代號。

官方解決方案

Dataram 也意識到這個問題,於是在軟件的用戶手冊中提出了一個解決方案,我稱之為官方解決方案。方案如下:先啟動軟件,設定好虛擬磁碟,並在控制台中將虛擬磁碟的代號設定為想要的代號,然後將虛擬磁碟的內容儲存成硬碟中的一個映像檔,然後每次啟動軟件的時候,自動載入這個映像檔,(有點像電腦遊戲中的“儲存”和“載入”的功能),那麼就可以每次都恢復相同的設定,包括磁碟代號。

但是,這個官方解決方案有兩個弊端,使得我個人認為比不用還好。

  1. 這個方案需要在磁碟機中建立一個與虛擬磁碟大小相同的映像檔。(雖然最新版本可以對映像檔進行壓縮,但是每次執行都要進行解壓縮,是用時間來空間,與使用 RAMDisk 的意願不符)
  2. 每次啟動軟件時,都需要載入這麼大的映像檔,既費時,又增加硬碟讀寫消耗,與RAMDisk 使用的意願不符。

我的方案:使用 DOS Batch 程序

為此,我專門寫了一個小小的 DOS Batch 程序,(如不計算程序中的註解的話,真正的程式碼只有三行)。這個程式使用了 Dataram RAMDisk 的一個特性功能:允許用戶指定磁碟機名稱(Drive Label),磁碟機名稱與磁碟機代號不同。對於系統而言,是使用磁碟機代號(C:, D:, E: …)來代表不同的磁碟機,但對於用戶而言,我們可以使用磁碟機名稱(“SystemDisk”, “MediaDisk” …)來表示不同的磁碟機。在Dataram RAMDisk,用戶可以為虛擬磁碟指定一個每次啟動都保持不變的磁碟機名稱。我的方案原理就是:每次啟動時,以指定的磁碟機名稱找出磁碟機代號(動態分配的代號,如 K:),然後建立另一個指定代號(如R:)的磁碟,將這個動態分配的代號(K:)與指定的代號(R:)進行關聯。那麼,無論我們打 “K:\Cache” 還是 “R:\Cache”,系統都會連到相同的目錄。

詳細步驟

為了將事情簡單化,我在這裡列出每一步步驟。

1. 建立一個 DOS Batch 檔案

用“記事本”建立一個文字檔案,敲入以下的程式碼,然後儲存為 C:\StartUp.bat(當然您可以使用其他名稱或儲存在其他地方)。由於“斷行”在 DOS Batch 檔案中是代表程式碼完結,故此,千萬不要隨意在這些程式碼中間加入“斷行”,否則可能會造成程式不能執行。

@rem assign the key variables.
SET _label=RAMDISK
SET _ramdrive=R:

@rem Below is the magic happens : Find the drive letter which has volume name set as _label, and mount it to _ramdrive
FOR /F "skip=1 tokens=1 delims=: " %%a IN ('wmic logicaldisk where "VOLUMENAME='%_label%'" get caption') DO ( subst %_ramdrive% %%a:\ )

@rem if you want to copy some files or mount your cache folders, do it after this line.

以下是關於這段程式碼的詳細說明,如果您對 DOS Batch 程式碼沒有興趣,絕對可以跳過這段,直接進行步驟2。

這一行:

wmic logicaldisk where "VOLUMENAME='RAMDISK'" get caption

是要根據磁碟機名稱 (VOLUMENAME) 找出磁碟機代號 (caption),執行結果如下:

Caption
K:

這個結果是以字串形式給出的,然後我們透過一個 for-loop ,逐行分拆這個結果,第一行 (caption) 可以跳過,第二行就是我們想要的東西。我們要將第二行用冒號 “:” 拆開,得出 “K” 和 “”(空字元) 兩個元素,按後將第一個元素 “K” 儲存到變量 %%a 之中。那麼我們就可以用 %%a 變量去代表這個動態分配的磁碟機代號了。

關於這個 for-loop 的解釋:

“skip=1” : 跳過第一行 (”Caption”)。第一行文字並沒有我們需要的東西。

“tokens=1” : 對於每一行文字,我們只取出以 “:” 分拆後的第一個元素。

“delims=:” : 使用冒號 “:” 來分拆每一行文字。

於是,最後我們取得動態分佈的磁碟機代號,並儲存在 %%a 變量中,然後在之後的 subst 程式碼中,我們實際上是執行這樣的指令:

subst R: K:\

這指令是讓系統知道,R: 就是 K: 的另一個名稱,以後無論是用 “R:” 還是 “K:”,兩者這實際都對應到 K: 的地方。

2. 指定虛擬磁碟機的名稱

打開 Dataram RAMDisk Configuration Utility (設定工具), 在 “Disk Label” 中打勾,並在下面輸入 “RAMDISK”

當然您也可以用其他名稱,但必須在上面的 DOS Batch 程式碼中作出修改:

SET _label=TheNewDriveLabel

3. 指定開機時執行上述程式碼

我們需要告訴電腦,在開機時自動執行步驟一的程式碼。

3.1 按 Win + R 組合鍵 (或在 “開始” 選單中按 “執行…”), 這時會出現一個 “執行…” 的對話框,輸入 “gpedit.msc” 並在結果中選擇 “gpedit” 。

注意:在某些Win7版本中(如 Win7 Home Edition),gpedit.msc 並沒有被納入系統中,在此情況下,我們需要使用 Windows 自帶的工作排程器(Task Scheduler)去讓Windows在啟動時自動執行上述的程式碼。關於工作排程器的詳細操作請見 http://windows.microsoft.com/zh-TW/windows7/schedule-a-task

3.2 在 “Group Policy Editor”(本機群組原則編輯器)中, 按 “Local Computer Policy”(本機電腦原則) -> “Windows Settings”(Windows 設定) -> “Scripts(Startup/Shutdown)” (指令碼(啟動/關機)),在編輯器右面,按兩下 “Startup” (啟動)

Startup script

3.3 在 “Scripts” (指令碼) 頁簽中,點擊 “Add…” (新增),然後將 “C:\StartUp.bat” (也就是步驟一建立的那個程式檔)加入到清單中,按 “OK” (確定) 完成。

4. 大功告成

現在,您可以重新開啟您的電腦,然後會發現“我的電腦”除了原來的虛擬磁碟機 (假設為K:)之外,還會多了一個 “R:”,打開 R: 磁碟機,內裡的內容與 K: 完全一樣。事實上,兩個代號其實都是指向相同的檔案,故此你在其中一個磁碟機中做的任何動作,在另一個磁碟機也會一樣改變。更重要的是,現在無論您的虛擬磁碟機是什麼代號,您的電腦中永遠都可以用 R: 來代表這個虛擬磁碟機了。

總結

這個方法雖然看上去好像很多步驟很繁複,但是實際上是比官方解決方案更加簡便。因為這個方法:

第一,只是三行程式碼,執行所需時間 0.000001 秒也不到,不會拖慢開啟速度,

第二,不會產生龐大的磁碟映像檔,因此可以減少對硬碟機的讀寫損耗,也不會被映像檔平白佔用了硬碟空間。

第三,也是最重要的,是以後開機不需載入龐大的映像檔,大大加快開啟的速度。

最後,這個方法也不是沒有缺點。由於程式是透過 subst 指令去將兩個磁碟機代號鏈接起來,故此,即使我們只需要 R: 來存取虛擬磁碟,但是虛擬磁碟的原來磁碟代號(K:)也會一併保留起來,故最後在我的電腦中您會看到有 R: 和 K: 同時並存。暫時我還是無法找出方法可以將 K: 隱藏起來。在本文章的英文版中,Merjin 讀者留言提出一個用  “diskpart” 取代 “subst” 指令的方法,那是直接將 K: 更改為 R:(而不是建立磁碟鏈接)。那是一個更好的方法,能夠解決上述兩個磁碟機並存的問題。我個人並沒有嘗試,有興趣的讀者可以去那篇文章中的讀者留言看看有關詳情。

歡迎各位讀者留言討論。

Fix the Dataram RAMDisk driver letter – with sample scripts

[Note: 我們另外為此文章撰寫了中文版本,請看這篇。]

Background

Since I wrote the post of Fixing the drive letter for RAMDisk without saving and loading drive image – A solution for Dataram RAMDisk, I received more response than I expected. From all the response, I find it really hard to explain without real sample scripts. So now I’m try to solve it by showing a sample script.

This post is supposed to give supplementary information about the original post. If you want to know more about the idea and technical details, you’re highly recommended to read the original post.

In this post I provides two sample scripts, one for the PC startup and one for shut down. I use my own PC’s setting to demonstrate the idea. So in order to run the script in YOUR PC, you may either change your PC’s setting to fit mine or change the script to fit your PC.

Here is some important assumptions / PC settings that you may need to know before we start:

  1. It assumes the disk label for the Dataram RAMDisk is “RAMDISK” (case-sensitive)
  2. It fixes the RAMDisk drive letter as R: .
  3. It loads the files stored in C:\MyRAMDiskFiles to the R: automatically. These files are the programs that I want to run at RAMDisk for better performance. For example, I put my Firefox at C:\MyRAMDiskFiles\Programs\Firefox\Firefox.exe,  So after the script, I can always launch firefox in R:\Programs\Firefox\Firefox.exe
  4. I also have Google Chrome installed in C:\Program Files. However, Chrome will always stores its cache files in the location where it installed. This script will mount the cache folder in the Chrome’s installation path to the RAMDisk drive, i.e.  R:\TEMP\BrowserCache . This can reduce the harddisk I/O frequency, and after the PC shutdown, the cache files will be erased automatically.
  5. Since I may change the program settings over time (e.g. updating the firefox extensions), I want these changes being maintained every time I starts the PC. So I want SOME files in the RAMDisk drive be saved to harddisk when I shutdown the PC.
  6. Also, all my downloaded files are stored in RAMDisk (R:\Download), but I don’t want to lose these files after reboot. So this script will also copy the files in R:\Download to C:\Download during PC shutting down.

Let’s start

Step 1

Create the two scripts: StartUp.bat and ShutDown.bat, and store them in C:\ (or whatever you like, as you will specify the files in latter steps)

Note: Line-break has important meaning in DOS Batch script, so if you copy & paste the script from this post, you must check if there is any extra line-breaks added by the text editor. Instead, you may consider download the zipped scripts : RAMDisk fix drive letter - sample scripts (Download is recommended.)

Script 1: C:\StartUp.bat

@echo off

@rem assign the key variables, see assumption #1 & #2
SET _label=RAMDISK
SET _ramdrive=R:

@rem Find the drive letter which has volume name set as RAMDISK, and mount it to _ramdrive
FOR /F "skip=1 tokens=1 delims=: " %%a IN ('wmic logicaldisk where "VOLUMENAME='%_label%'" GET CAPTION') DO (
	subst %_ramdrive% %%a:\
)

@rem initialize the ramdisk - automatically copy the files in C:\MyRamDriveFiles to the ramdisk after the Windows starts. see assumption #3
mkdir %_ramdrive%\Programs
xcopy C:\MyRamDriveFiles\*.* %_ramdrive%\Programs\ /E/Q/Y/R

@rem link the google chrome cache folder to ramdisk. Remeber to replace [Your-Windows-user-name] with your own, see assumption #4
mkdir %_ramdrive%\TEMP\BrowserCache
mklink /j "C:\Users\[Your-Windows-user-name]\AppData\Local\Google\Chrome\User Data\Default\Cache" "%_ramdrive%\TEMP\BrowserCache"

@rem create a dir in ramdisk to stores the downloaded files, you will need to set up Firefox to store downloaded files in R:\Download, see assumption #6
mkdir %_ramdrive%\Download

@echo on

Script 2: C:\ShutDown.bat

@echo off

@rem see assumption #2
SET _ramdrive=R:

@rem copy all program files back to harddisk -  see assumption #5
xcopy %_ramdrive%\Programs\*.* C:\MyRamDriveFiles\ /Q/Y/R/S/D/C

@rem copy all downloaded files back to harddisk  -  see assumption #6
mkdir C:\Download
xcopy %RAMDISK%\Download\*.* C:\Download\ /S/D/Q/R/C

rem disconnect the google chrome cache folder linking -  see assumption #4
rmdir "C:\Users[Your-Windows-user-name]\AppData\Local\Google\Chrome\User Data\Default\Cache"

rem delete the drive mounting
subst %RAMDISK% /D

echo on

Step 2

Go read the original post and do step 3 in that post.

Step 3

Now you have added the StartUp.bat script to the “Group Policy Editor”, so go back to step 3.2 in the original post, repeat the step but click ok “ShutDown” instead of “StartUp”, and add the “C:\ShutDown.bat” to the list.

So now you have “C:\StartUp.bat” added to “Startup” and “C:\ShutDown.bat” added to “Shutdown”.

Startup script

Step 4

Make sure you have met the assumptions above, and check if you have all the “C:\MyRAMDiskFiles” and “C:\Download” folders created. Then restart your computer and enjoy the RAMDisk.

Comments are welcomed!

JavaScript: how to load dynamic contents (HTML String, JSON) to iframe

The story

Although people are suggesting the replacement of <iframe> by <div> due to the poor usability of <iframe>,  there are still some cases that <iframe> is the only way to go.

Consider such case : you want to show a preview screen before the user hit “submit” button on a page with form (the data input page). When the preview button is hit, an ajax request is sent to the server asking a validation of the user input. Then the server either generates the preview page HTML code (if the input is valid) or error message (if the input is not valid), in JSON format. The client receives the JSON response. If the JSON is an error message, then the client alerts user the error, otherwise, presents the preview screen (the HTML codes in JSON).

All of these seem very straight forward, until the time that you are presenting the preview page HTML codes. As the preview page HTML is a full set of HTML code, including the <html>, <head> and <body>tags, and more importantly it includes a new set of CSS styles and JavaScript codes. If you present these codes inside a <div> tag, the new CSS styles and JavaScript codes will definitely interferer the CSS styles and JavaScript codes of the data input page, making the both preview screen and the data input page extremely awful.

In such case, the proper way is to present the preview screen as an independent section from the data input page. This is where <iframe> should be used instead of <div>. Everything in <iframe> is independent from its parent document, so the <iframe> can have its own <doctype>, <html>,<head>, <body>, and CSS styles and JavaScript.

The problem

However, according to the specification of <iframe>, the content of <iframe> is specified by the “src” attribute which accepts values in URL format, like “http://www.something.com/”. It cannot load dynamic HTML codes. Moreover, as <iframe> is treated like an independent section from the current page, JavaScript frameworks such as jQuery has limited ability to modify its contents: you can only select and modify the contents inside the <body> of the <iframe> contents, you can do nothing outside the <body>, not to mention the jQuery ready() function doesn’t even work properly for <iframe>.

The solution

However, after some studies on the relationship between <iframe> and its associated document contents and combined the discussion on the web, we successfully inject HTML codes into an <iframe>. Here is how we do that:

<html>
    <head>
    </head>
<body>
    <h1>Test iframe</h1>
    <iframe id="test_iframe" src="about:blank" width=400 height=400></iframe>

	<button onClick="javascript:injectHTML();">Inject HTML</button>
</body>

<script language="javascript">
function injectHTML(){

	//step 1: get the DOM object of the iframe.
	var iframe = document.getElementById('test_iframe');

	var html_string = '<html><head></head><body><p>iframe content injection</p></body></html>';

	/* if jQuery is available, you may use the get(0) function to obtain the DOM object like this:
	var iframe = $('iframe#target_iframe_id').get(0);
	*/

	//step 2: obtain the document associated with the iframe tag
	//most of the browser supports .document. Some supports (such as the NetScape series) .contentDocumet, while some (e.g. IE5/6) supports .contentWindow.document
	//we try to read whatever that exists.
	var iframedoc = iframe.document;
		if (iframe.contentDocument)
			iframedoc = iframe.contentDocument;
		else if (iframe.contentWindow)
			iframedoc = iframe.contentWindow.document;

	 if (iframedoc){
		 // Put the content in the iframe
		 iframedoc.open();
		 iframedoc.writeln(html_string);
		 iframedoc.close();
	 } else {
		//just in case of browsers that don't support the above 3 properties.
		//fortunately we don't come across such case so far.
		alert('Cannot inject dynamic contents into iframe.');
	 }

}

</script>
</html>

We have tested this code with Firefox 3.5 / 4 / 5, IE 6,7,8,9 and Chrome and fortunately all of them supports the dynamic HTML loading with this method.

Yet another Javascript Internationalization (i18n) module

i18n?

Internationalize (a.k.a. i18n) is a very basic practice when working with multi-lingual websites. A simple i18n mechanism involves only a dictionary containing the pairs of strings, and a dictionary lookup function (e.g. the underscore magic function _() ). Once these two things are ready, the implementation is relatively simple. Major programming frameworks also provide their own i18n functionality, or if it doesn’t, there is also a mature module called “gettext” available to help.

Well, all these are true when talking about server-side programming.

What about client-side i18n?

For client-side it is unavoidable to use JavaScript. There are some very nice i18n modules available for different JS frameworks. However, after using some of them, I’m not satisfied, because of one or more of the following reasons:

  1. Work with string identifiers(i.e. __(‘this_is_my_string’); ), but doesn’t work with full sentences (i.e. __(‘this is my string!’); ).
  2. Heavy weighted, even excluding the dictionary file.
  3. Dictionary needs to be pre-compiled.
  4. Cannot work without the JS framework. (Framework is heavy!)

So at the end I wrote my own, and call it “jsIn” (javascript Internationalization).I also created a single page for this module for the documentation purpose. If you’re interested in this tiny (1KB) little toy, please visit here.

What makes jsIn different?

I’m not saying jsIn is totally different from some others, because I don’t have the time to test all the available i18n solutions to see the difference. The methodology in jsIn is very simple (as mentioned before, the most simple form of internationalization is just 2 things : a dictionary file and a lookup function.),  that some others may have already been using it. If this is the case what I can say is “coincidence”.

There are some features that I can’t find them all in any single solution but jsIn:

  1. Work with both string identifier and full sentence translation.(And string identifier gives extra performance boost.)
  2. Light weight, actually it’s tiny weight: just 1.3KB after minified, excluding the dictionary files.
  3. Standalone : no frameworks required. Actually you can even put the script in the header so that your strings can be translated before it shows to the visitors.
  4. Global magic function. Just call   __(‘string to translate’) anywhere you like, even inside a jQuery plugins!

It also passed our unit test page and is used in production server now.

PHP isset() and multi-dimentional array

** The issue discussed in this post has been fixed since PHP5.4.0, so the below discussion and solution are for PHP 5.3.x or lower. Thanks David for clarifying.

A few weeks ago I covered how to check the existence of an array element in PHP. In the post I explained why isset() is dangerous to check the existence of elements in an array. I also proposed a better solution (the isset()+array_key_exists() method) to do the checking.

Today I’m going to discuss another strange (and dangerous) behavior brought along with isset() function and multi-dimensional arrays.

The problem

Let’s consider this simple code:

<!--?php $a = array('test'=-->'ABC');
var_dump(isset($a['test']));                       //true
var_dump(isset($a['test']['non_exist']));          //true?!!
var_dump(isset($a['test']['non_exist']) || array_key_exists('non_exist', $a['test'])); //true again?!!!
?>

Surprise, huh? Isset() returns true for a non-exist element!

What even worse is that the previous proposed method (the isset()+ array_key_exists() method) also gives a wrong result! This is because isset() returns true for the non_exist element so the overall OR operation will become “true”. The array_key_exists() is never implemented.

The reason

So why isset() returns true for a non-exist element? I’m not sure the exact reason but I have a guess:

PHP first look at $a[‘test’]. Since $a[‘test’] does exist, isset($a[‘test’]) returns true. Then PHP checks the 2nd dimension: the ‘non_exist’ element. As $a[‘test’] is a string, it is also considered as an array (In PHP, string is a sequential array by type-casting). When checking the sequential array where all index should be integers, the index [‘non_exist’] is **converted** to an integer which equals zero. So actually PHP is checking isset($a[‘test’][0]). Unfortunately $a[‘test’][0] does really exists (with value ‘A’). So the overall result of this checking is “true”.

To verify this guess, let’s run this code:

<!--?php $a = array(1=-->'', 2=>'ABC');
var_dump(isset($a[1])); //true
var_dump(isset($a[1]['t'])); //false => $a[1] is empty string, $a[1][0] doesn't exist
var_dump(isset($a[2])); //true
var_dump(isset($a[2]['t'])); //true => $a[2] is 'abc', so $a[2][0] exists and equals 'A'.
?>

The result has shown that my guess is pretty reasonable.

The solution

You say: OK, I know your guess is somehow right, so how to fix it?

Usually when we check the existence of elements in multiple dimensional array, we use  something like

array_key_exists('non_exist', $a['test']); 

Yes. This is true…but if you really do so in our case, you will get this warning:

Warning: array_key_exists() expects parameter 2 to be array, string given 

Somehow for unknown reason array_key_exists() doesn’t consider string as array now and is complaining us.

So what’s the solution?

Complete array element existence checking function

Combined with what I proposed in the previous and this post, I have worked out a function that checks whether an element does exist in an array, regardless the array’s dimensions:

<!--?php function elementExists($key, $array){     if (is_array($key)) {         $curArray = $array; 		$lastKey = array_pop($key); 		foreach($key as $oneKey) { 			if (!elementExists($oneKey, $curArray)) return false; 			$curArray = $curArray[$oneKey]; 		} 		return is_array($curArray) && elementExists($lastKey, $curArray); 	} else { 		return isset($array[$key]) || array_key_exists($key, $array); 	} } $a=array(1,2,3,4, 'dim1'=-->array('dim2'=>array('dim3'=>null)));

//multi-dimension : check if $a['dim1']['dim2']['dim3']['dim4'] exists:
var_dump(elementExists(array('dim1', 'dim2', 'dim3', 'dim4'), $a)); //false

//multi-dimension : check if $a['dim1']['dim2']['dim3'] exists:
var_dump(elementExists(array('dim1', 'dim2', 'dim3'), $a)); //true

//single dimension : check if $a['dim1'] exists:
var_dump(elementExists('dim1', $a)); //true
?>

This piece of codes looks quite awful and dirty, and its performance  is not evaluated. I think there are more elegant (and faster) codes to do the same thing. Since I’m in a hurry and got to complete my project ASAP, I prefer to leave it as it is now.

Comments are always welcomed!

Internet Explorer 8 background disappeared (or becomes white) bug with jQuery 1.6 (or 1.6.1)

Latest Update: This bug has been fixed by jQuery 1.6.2 released on 30/06/2011.

If your project has upgraded to jQuery version 1.6 or jQuery 1.6.1, then you should make sure it works fine with Internet Explorer 8 (IE8) before you hit the publish button. This is my two cents.

What’s wrong with jQuery 1.6+ and IE8?

If your website has used background image or color other than “white”, and if it is viewed by IE8, the website will be rendered just right…but only at the first sight…and then the background will goes plain WHITE soon after that, automatically. Only IE8 will show you this “magic”.

There is a bug in jQuery 1.6 and 1.6.1 and is reported here. Unfortunately we have suffered this strange behavior for several days before we realize that this “magic” is associated with jQuery.

Solution

There is also a solution suggested in the above mentioned bug report. To save your time, I quote it here with credit goes to the original author:

Same problem on version 1.6.1

The bug can be resolved on ligne 1288 from jquery-1.6.1.js.

Replace :

“documentElement.insertBefore( body, documentElement.firstChild );”

by

“documentElement.appendChild( body );”

Hope this post could save you a day or several hours. Happy coding!