在 SharePoint 裡想要作 Hit Track,最先想到的當然是使用內建的 Usage analysis,但是雖然把這個功能開啟,也未必能得到你需要的資料。就這次的 Case 來說,開啟 SharePoint 的 Usage analysis 功能之後,居然找不到它 Log 的 DB/Table。最後只好自己想辦法客製一個 Hit Track 的功能出來囉。
首先第一個想到的是從 IIS Log 著手,如果 IIS 有正確的 Log 到使用者所點選的 URL,由於新版的 IIS 可以直接設定將 Log 資料儲存到 SQL Server 上,這樣我們就可以很輕易的作到所需要的功能。很可惜,設定測試之後發現 SharePoint 的 ISAPI Extension 在 IIS 處理前已經先攔掉,所以 IIS 只能記錄到 /sites/sitename/default.aspx 或是 /_layouts/xxx/AllItems.aspx 這樣的 URL,而存放在 List, Document Library 的文件點選則是完全沒有 Log,殘念。
既然不能夠由 IIS 著手,那麼我自己動手作一個 Log 總行吧? 問題是這個 Log 要從哪裡攔?與崔佛進行討論,提供了兩個可行性方案,一個是 Global.asax,另一個則是使用 HTTP Handler,這兩個都是可以嘗試的。我先在自己的環境上先把 SQL Server 裡的 Table 建好,然後寫了這麼一段 Code 在 Global.asax 進行測試:
public void Application_BeginRequest() {
string sDB_Host = “sqlserver”;
string sDB_User = “sa”;
string sDB_Pass = “sapass”;
string sDB_Data = “LOGTEST”;
string strConn = “database=” + sDB_Data + “;server=” + sDB_Host + “;user id=” + sDB_User + “;password=” + sDB_Pass + “;”;
SqlConnection conn1 = new SqlConnection(strConn);
conn1.Open();
string sDatetime = System.DateTime.Now.ToString(“u”);
sDatetime = sDatetime.Substring (0,sDatetime.Length – 1);
string sUsername = “”;
try {
sUsername = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
} catch {
sUsername = “”;
}
string sURL = Request.ServerVariables[“PATH_INFO”].ToString();
string sSQL = “INSERT INTO tblHitLog VALUES(‘” + sDatetime + “‘,'” + sUsername + “‘,'” + sURL + “‘);”;
SqlCommand sqlCdCmd1;
sqlCdCmd1 = new SqlCommand(sSQL, conn1);
SqlDataReader sqlDrReader1;
sqlDrReader1 = sqlCdCmd1.ExecuteReader();
sqlDrReader1.Close();
conn1.Close();
}
測試一般文件 .txt 及 .doc 發現這段 Code 執行無誤,有正確的把資料 Log 到 SQL Server 裡頭。接著再測試一下 HTTP Handler,寫了一段簡單的程式進行測試,其實只是把上面那段 Code 給拷貝到 HTTP Handler 程式去編譯。然後設定一下 web.config 加上:
<httpHandlers>
<add verb=”*” path=”*” type=”JJHandler.MyHttpHandler,JJHandler” />
</httpHandlers>
重啟 IIS,測試,發現只有 Global.asax 所 Log 的那一筆,HTTP Handler 並沒有作用,檢查,發現少了一個設定步驟:進入 IIS 管理–網站–應用程式設定,加上 “*”, 對應到 ASP .NET 所對應的 ISAPI Extension 去(每台機器可能都不一樣,由 .aspx 這一項複製全路徑+檔名即可。再測試,成功的 Log 進 SQL Server。
本來以為這樣已經可以了,因為兩個測試都已經通過,隨便選一個效能比較好的來實作就行了,沒想到把這兩個測試的程式套到 SharePoint 的 LAB 環境上頭後,發現在有安裝 SharePoint 的機器以上兩種方式都沒有正常的 Log 資料,真是晴天霹靂。仔細檢查原因,發現有可能是因為 SharePoint 在 IIS 上使用的 stsfltr 這個 ISAPI Filter 會攔在 ASP.NET 的 Parser 之前,所以 Document Library 跟 List 裡的 Hit 都完全攔不到了。再度殘念。
既然偷懶的方法都不可行,只好真的全手動的來客製化這一段 Log 了。再找崔佛討論過,也許可以利用網路 Sniffer 的方式抓 User Request 過來的 HTTP GET,這是方法一,不過由這個方式不能攔到使用者的資料,因為我們除了 Log URL,還要 Log 到使用者的 ID,而伺服器會使用 Windows 整合式認證,配合著 AD Server 的帳號管理,中間傳送的使用者資料是加密過的,無法使用 Sniffer 抓到。方法二,就是寫一個 ISAPI Filter,看看能不能攔在 SharePoint 的 ISAPI 之前,但是依照以前的經驗,寫 ISAPI Filter 是一個痛苦的事,再加上要在裡頭使用 ADODB,那樣更無法在預定的時間上達到目的。而方法三則是在所有的 HTML Page,利用 Javascript 去產生一個 HTTP Session,呼叫一支 ASP 來 Log,只要傳送使用者點選的 URL 過去即可。
看來只有第三種方法可以解決燃眉之急,因為它是能夠在最短時間內完成的。首先考慮到的是,在所有的 HTML Page 要插上 Javascript Code 的問題,因為部分 SharePoint 頁面經過客製化,儲存在 SQL Server 中,而不是未客製化時存在 Template 檔裡,所以要作到在所有頁面插入 Code,必需手動的一個一個加?這太累了,得找到一個偷懶的方法,嘗試著由 Global.asax 著手。
測試 Javascript:
<script language=javascript>
alert (“Here”);
</script>
首先先將這一段 Code 插在 Global.asax 的 Application_AfterRequest(),執行後看看結果,發現它根本沒有把這段 Code 插進去,再換到 Application_BeginRequest() 測試,發現 Code 插進去了,可是 “</script>” 這段文字被打亂了,所以程式並沒有執行。這應該是因為安全性的考量,所以 ASP.NET 特意濾掉這個 “</” 字串。嘗試欺騙 ASP.NET 的 Parser 吧,否則我真的得寫 ISAPI Filter 了嗎?發現下面這一段 Code,可以欺騙過 ASP.NET 的 Parser,爽!
騙過 ASP.NET Parser 的程式碼:
Response.Write (“<“);
Response.Write (“/”);
Response.Write (“script>”);
接下來,得處理所有的 <A> Tag,必需要將所有的 <A> 的 OnClick 都 Bind 上我的 function 作為 OnClick 事件的 Handler 才行。我嘗試寫了這麼一段 Code:
<a href=”#A1″ name=”a1″>A1</A><br>
<a href=”#A2″ name=”a1″>A2</A><br>
<a href=”#A3″ name=”a1″>A3</A><br>
<a href=”#A4″ name=”a1″>A4</A><br>
<a href=”#A5″ name=”a1″ onclick=”javascript: OldClick();”>A5</A><br>
<script language=jscript>
function LinkClicked()
{
var objSource = window.event.srcElement;
alert(‘New Click!!’ + objSource.href);
}
function OldClick()
{
alert(‘Old Click!!’);
}
for (var i=0;i<document.anchors.length;i++) {
document.anchors[i].onclick = LinkClicked;
}
</script>
可是測試的時候發現,如果 <A> 沒有指定 name property 的話,是不會算在 Anchor Array 裡頭的!而以前所寫的 Webpart 並沒有在所有的 Anchor 都加上 name,基本上也沒有人會這麼無聊,把每一個 Anchor 都取個名字,沒辦法,這麼說的話,就不能由這段程式碼來作,因為不可能再花時間去翻以前的 Webpart Code。另外使用這段程式碼還會有另外一個缺點,那就是如果先前的 Anchor Tag 中已經有了 OnClick 事件,那我要怎麼去補綁我的事件?Java 可沒有 .onclick += myfunction; 這種指令呀。換另外的方法作。
由崔佛給我的 Sample Code 加上討論的結果,決定用他的 document.onclick 來取代 anchor array 的 onclick,然後經由判斷該 Click 是不是由 Anchor 觸發來進行下一步的動作,測試結果很令人滿意。底下是加在 Application_BeginRequest() 中的 Code:
Response.Write (“<script language=”javascript”> \n”);
Response.Write (“function TrigerLog(sLink) \n”);
Response.Write (“{ \n”);
Response.Write (” var xmlhttp = new ActiveXObject(“Microsoft.XMLHTTP”); \n”);
Response.Write (” sURL = “http://sps2003/_layouts/ICM_Log/loganchor.aspx?LINK=\” + sLink; \n”);
Response.Write (” xmlhttp.open (“GET”, sURL, false); \n”);
Response.Write (” xmlhttp.send(); \n”);
Response.Write (“} \n”);
Response.Write (“function LinkClicked() \n”);
Response.Write (“{ \n”);
Response.Write (” var objSource = window.event.srcElement; \n”);
Response.Write (” if (objSource.tagName == “A”) { \n”);
Response.Write (” TrigerLog(objSource.href); \n”);
Response.Write (” } \n”);
Response.Write (“} \n”);
Response.Write (“document.onclick=LinkClicked; \n”);
Response.Write (“<“);
Response.Write (“/”);
Response.Write (“script>”);
在這段步驟進行中還有另外一段插曲,就是本來的 TrigerLog 是寫成 VBScript 的方式插入,結果發現 VBScript 如果在 <HTML> Tag 的前面,就會使網頁發生錯誤,而如果是 Javascript 的話就不會。慶幸當出在測試 Global.asax 插入 script code 的時候是用 Javascript 作測試,否則就會倒得很冤枉了。
以上,作為記錄。特別感謝 Microsoft TAIWAN MCS 崔佛的幫忙及提點才得以在短時間內完成這個工作。另外,如果有時間的話應該來實作一個 ISAPI Filter 的 Log 程式,因為這才是正解呀!