這篇拖好久了,本來上禮拜要寫,結果正好碰到 BlogEngine 1.4 RELEASE,就一直拖到現在...。之前找到一個給 BlogEngine 用的 Counter Extension,以功能來說還不錯用,不過用久了就開始不滿足了。正好翻到這篇教學文章,算是官方文章了吧 (BlogEngine 作者之一寫的教學文)? 所以就動起自己寫的念頭。舊的其實沒什麼不好,不過缺了這幾項我想要的功能:
既然要重寫,當然要寫個合用的. 底下是我對於新的 COUNTER 期望:
決定好後就動工了! 既然問題都圍繞在 data storage 上,先來看看原來的檔案格式:
1: <?xml version="1.0" encoding="utf-8" standalone="yes"?>
2: <posts>
3: <post id="b43ec49e-e9a2-4696-bcc7-2ba1667ecda9">781</post>
4: <post id="f1411c11-11ed-4f35-b383-0c6c8b2b963a">603</post>
5: <post id="e7b57492-652b-4247-bcd4-bc3ac2e56318">589</post>
6: <post id="7e2c2c88-240c-40ea-8477-2c96880adc8e">556</post>
7: <post id="0fda9c32-d294-4f09-85cd-41dab8e677cb">678</post>
8: ......
9: </posts>
很普通的格式,配合我的需求,新的檔案結構我打算改成這樣:
1: <?xml version="1.0" encoding="utf-8"?>
2: <counter base="8828">
3: <hit time="2008-06-29T12:42:51" referer="" remote-host="66.249.73.185" user-agent="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" />
4: <hit time="2008-06-29T13:04:15" referer="http://www.google.com.tw/search?complete=1&hl=zh-TW&cr=countryTW&rlz=1B3GGGL_zh-TWTW237TW238&q=%E9%A6%99%E6%B8%AFg9&start=30&sa=N" remote-host="124.10.1.162" user-agent="Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9) Gecko/2008052906 Firefox/3.0" />
5: <hit time="2008-06-29T13:04:20" referer="" remote-host="66.249.73.185" user-agent="Mediapartners-Google" />
6: ......
7: </counter>
其中, /counter/@base 是給你作弊用的,這數字你填多少就是多少,然後再加上底下有幾筆 <hit /> 記錄,總數就出來了。很簡單的格式,而流水帳就是記在 <hit /> 這個 XML ELEMENT 的 ATTRIBUTES 上。目前記的有時間,IP,REFERRER 而以。等到網站人數暴增 (幻想嘛?),COMPACT的動作就免不了了,預留的 /counter/@base 就派上用場了。COUNTER 在必要時就能刪掉多餘的 <hit /> 記錄,再把刪掉的筆數加到 /counter/@base 上,讓最後點擊總數不變,又能控制記錄檔的大小。
邏輯確定後就可以開始動工了,不過另外要解決技術問題,就是 ThreadSafe 的部份。這部份讓我煩惱了一下,因為在 File System 就提供了 FILE LOCK 的機制可以用,不過取得 LOCK 失敗是會引發 EXCEPTION,而不是像其它的 THREAD 控制機制會 WAIT。因此最後我決定 FILE LOCK 的機制做第二層保障 ,主要的 LOCK 機制還是自己來,用基本的 Monitor 來實作。要實做 LOCK 的前題,是要有明確的 LOCK "對像"。兩個 thread lock 同一個物件,後面 lock 的 thread 會被 block 暫停執行,直到前一個先 lock 同一個物件的 thread 釋放之後才會被喚醒,因此要先解決兩個同 ID 的 COUNTER 能拿到同一個物件,才能實作 LOCK 機制。這物件就好幾位合適的候選人,第一種方法是 COUNTER 本身,第二種方法則是替每個 counterID 產生一個無用的物件,就單純拿來 LOCK 用。
要用 (1) 成本太高,一來就必需實作 Flyweight 這個 design pattern,這個設計模式並不難,只要實作 factory pattern再搭配個 Dictionary<string, Counter> 物件就可以搞定,但是我最後沒有選擇這個作法。因為最糟的情況下有可能會讓整個系統可能會用到的 counter 通通被放到這個 dictionary ,沒有被釋放回收的機會,因為沒有明確的時間點可以把這物件從 dictionary 移掉,移不掉的話永遠就會留著一個 object reference 指向這 COUNTER 物件,reference 只要存在,它就永遠不會被 GC 收掉...。不過這還是有解,只要用 WeakReference 就可以解決了,不過我只是要作個簡單的 Counter,搬出這一堆東西會不會太過頭了?
因此我選擇了第二個方法。一樣用 flyweight pattern,只不過我放的是拿來 lock 的物件。我是直接 new object() 就拿來用了,物件很小我就不用耽心建立太多個又不能回收的問題...,而 counter 就讓它回歸最簡單的用法,需要就 new 一個出來,用完就丟著等著被回收。
每次都是講的話比 CODE 還多,來看看程式碼:
1: // 所有 COUNTER 用的 SYNC 物件 DICTIONARY
2: private static Dictionary<string, object> _counter_syncroot = new Dictionary<string, object>();
3: // 取得這個 COUNTER 用的 SYNC 物件
4: private object SyncRoot
5: {
6: get
7: {
8: return Counter._counter_syncroot[this._counterID];
9: }
10: }
11: private Counter(string counterID)
12: {
13: this._counterID = counterID;
14: //
15: // 建立 SYNC 物件 (如果沒有的話)
16: //
17: lock (Counter._counter_syncroot)
18: {
19: if (Counter._counter_syncroot.ContainsKey(this._counterID) == false)
20: {
21: Counter._counter_syncroot.Add(this._counterID, new object());
22: }
23: }
24: // 略 ....
25: }
26: public void Hit()
27: {
28: lock (this.SyncRoot)
29: {
30: //
31: // LOCK 後再開始更新檔案內容。 程式碼 略...
32: //
33: }
34: }
這問題解決掉後,剩下的就是單純把我要的邏輯實作出來,這部份我相信讀者的程度都不用我多講了,有需要的看 CODE 就看的懂。請直接下載最後面的程式碼就好。
另外一個要提一下的是跟 BlogEngine 本身 Extension 相關的,這部份也花了點時間研究該怎麼寫。BlogEngine 的 Extension 寫法比較特別一點,一般這種外掛都是採 Provider 的方式實作 ( factory pattern 加上 abstract class ),先定義好這個 Provider 能作什麼事,然後每個寫 Extension 的人就自己繼承這類別來修改,再靠 Factory 動態的建立正確的 Extension 物件來使用。不過在這部份 BlogEngine 採用完全不同的作法來設計它的架構: Event Handler。
Provider 依賴的是事先定義好的 ProviderBase (abstract class) 類別。這個類別定義了多少東西給底下的人覆寫,就決定了寫外掛的人能處理多少事。好處是簡單,架構清楚。缺點是能讓你擴充的功能,在 DESIGN TIME 就決定了,要多一個能 "擴充" 的地方,就得改 ProviderBase 類別定義,這很有可能會讓現有的 Extension 不能跑...。換成 EVENT 的方式就沒有跟程式碼綁的那麼緊了。多了新的功能,多定義一些事件就夠了。BlogEngine 就是採這種方式來實作它的 Extension ..
另外比較特別的是,BlogEngine 替每個 Extension 規劃好存放設定的地方。 1.3 版是所有的 Extension 共用一個設定檔, 1.4 則是有獨立的設定檔可以用。不過這些改變倒是沒有影響到它提供的 API。對於 API 來說,設定檔提供每個 Extension 一個像是 DataTable 那樣的 data storage, 讓你自訂欄位名,型別,然後能讓你一筆一筆的加進去,可以有多筆資料,而 BlogEngine Runtime 會負責幫你管理好這些設定。
這部份帶入門就好,用法還是去查官方文件比較快。我簡單貼一下這部份 CODE 跟畫面上提供的設定頁面給大家看看:
1: public PostViewCounter()
2: {
3: Post.Serving += new EventHandler<ServingEventArgs>(OnPostServing);
4: ExtensionSettings settings = new ExtensionSettings("PostViewCounter");
5: settings.AddParameter(
6: "MaxHitRecordCount",
7: "最多保留筆數:");
8: settings.AddParameter(
9: "HitRecordTTL",
10: "最長保留天數:");
11: settings.AddValues(new string[] { "500", "90" });
12: //settings.ShowAdd = false;
13: //settings.ShowDelete = false;
14: //settings.ShowEdit = true;
15: settings.IsScalar = true;
16: settings.Help = "設定 counter hit records 保留筆數及時間。只有在筆數限制內且沒有超過保留期限的記錄才會被留下來。";
17: ExtensionManager.ImportSettings(settings);
18: _settings = ExtensionManager.GetSettings("PostViewCounter");
19: }
對應的設定頁面:
最後講了半天,真正想自己動手寫的人應該不多吧 :D,只是想下載回去裝來用的人就不用聽我前面廢話一堆了,只要下載這檔案,放到 ~/App_Code/Extension 下,就安裝完成了... 咳咳,連安裝手冊都省了。檔案 COPY 好後就會在 Extension Manager 裡看到我寫的外掛,就可以開始用了。有任何意件歡迎留話給我 :D
檔案下載: http://columns.chicken-house.net/wp-content/be-files/PostViewCounter.cs
原本只是想找找有沒有 BlogEngine.NET Extension 存放自訂設定值的說明,無意間找到這個令人 Orz 的東西,BlogEngine.NET 專用的 Widgets ...
Widgets 這名字到處都有人用,指的不外乎是能留在桌面或是網頁的 "小工具"。拜物件技術所賜,這種東西越來越好作了。當年還在用 ET 的時代,一篇報告要插張圖,就有一堆技巧得拿出來用... 先下好控制碼把位子空出來,列印... 然後再印張圖,貼上去..
中間年代有些工具可以直接插圖的就不管了,真正有革命性的改變,是 WIN3.1 時代的 OLE (Object Linkage and Embedded) ,其實這也不是 Microsoft 先發展出來的,較早是 APPLE 的 OpenDoc ... 這些物件的技術開始可以讓兩種獨立的 AP,在同一份文件或是畫面上共同處理一樣的資料...。
幹嘛扯這些? 因為我碩士論文就是弄這些五四三的物件導向技術,哈哈...。沒什麼,只是看到 BlogEngine Widget 這麼好寫,沒幾行 CODE 就搞定,那當年我的研究論文跟本是寫好玩的.. @_@
http://rtur.net/blog/post/2008/03/24/BlogEngine-Widgets-Tutorial.aspx
原本要找 Extension, 後來逛到這位強者的網站,它擺了一段很簡單的 CODE,就是之前研究過一陣子的 FlickrNet。它寫了一個簡單的 USER CONTROL,透過 FlickrNet 抓幾張圖回來,顯示在 User Control 的範圍內。
另外他寫了另一個 User Control,用來編輯第一個 User Control 可能會用到的設定值,畫面上幾個 Text Box 也搞定了。
然後,放到 BlogEngine.NET 的 ~/Widgets/ 目錄下就好了? 簡單到想打人...
現在回頭來看看,這樣做出來的 widget 能幹嘛? 我現在用的版面,右側有一堆 "BOX",有 Google 的廣告,有 "安德魯是誰?",有最新回應...,沒錯,在 BlogEngine 1.4 之後,這些就變成真正的元件了。大概就像 Microsoft 的 Web Parts 一樣。網頁的主人可以很簡單的在畫面上拉一塊新的 "widget" 出來,直接用拖拉的方式放到喜歡的位子,按下 [EDIT],畫面就會切到編輯用的 User Control,OK就存檔...
要做到這堆事,你只要會寫 USER CONTROL,照 BE 的規舉,繼承指定的 CLASS,把檔案放到指定的目錄就好了... Orz
有時後東西弄的太簡單也很令人洩氣,尤其是 BLOG 剛搬完家,差不多要完工時 @_@,才發現 BlogEngine 在 2008/06/30 推出 1.4 版,主要就是加上 widget 的功能... 哈哈,那個小熊子應該比我更想哭吧 (H)
不過事情倒還很順利,之前有作好準備,花了一兩個小時試一下就動工了,動啥工? 看看底下的版本... 已經搬到 1.4 版了 :D,唯一美中不足的是,過去花太多工夫在這個樣板上面,所以只好連樣板一起搬過來... 而這個樣版並不支援 Widget 的功能。看來又有得忙了...
古早以前,曾替我的 BLOG 加上推推王的小貼紙,不過當時也僅止於把 CODE 加上去而以,成效不大好...。這次搬家搬到 BlogEngine 後,又開始一樣的循環了..,要不要加上這些共用書籤? 要加那一套? 目前台灣用的最多就是黑米跟推推王了。
原本挑了黑米,只因為它有提供 [黑米卡],正好取代掉 BlogEngine 右邊那塊 [關於作者] .. 不過試用的情況不怎麼理想,除了速度有點慢之外,同一頁放太多 (幾十個) 的速度也很慢,也許跟 BlogEngine 我選用的樣板有點不合,速度太慢時有時整個版面就毀了,下載到一半就掛掉...
相對之下,看了看 FunP 提供的 SCRIPT,看起來 CODING STYLE 比較合我的胃口,速度也快一些,沒碰到會讓我版面掛掉的問題。另外使用上的流程 FunP 也簡單一點,本來想兩家的書籤都放的,到最後就決定支持一下交大的學弟,就全力跟 FunP 推推王整合好了。
動手前先計劃了一下,毫無目的的把一堆 CODE 加上去,我最忌誨這樣弄了,看起來一點主題都沒有。常看到別人的 BLOG 滿滿一堆標籤,從國內的 FunP,黑米,MyShare,到國外的DELICIUS,還有一堆叫不出名字的,一字排開落落長...
BlogEngine 原本也有內建一些,不過被我拿掉了。底下列出我調整的前後差異:
原 CS 的樣式:
修改前:
修改後:
看了一下推推王的工具,不外乎都是插入一段 <SCRIPT> 標簽,然後用 document.write( ) 或是 eval 等等 client side script 的方式產生片 HTML Code, 缺點就是繞了一大圈,出了問題也常讓人搞不清楚問題在那裡。花了點時間追一下,追出最後插在網頁的 HTML CODE 長這樣:
1: <IFRAME
2: width=60
3: height=55
4: marginWidth=0
5: marginHeight=0
6: frameBorder=0
7: scrolling=no
8: src="http://funp.com/tools/buttoniframe.php?url=xxxxxxxxxxxxxx&s=1">
9: </IFRAME>
看起來就是直接產生一個帶著指定參數的 <IFRAME ...>,於是我在 BlogEngine Themes 版面就直接產生 <IFRAME> ...底下是 BlogEngine THEME 目錄下的 PostView.ascx 片段:
1: <%
2: Regex _stripHTML = new Regex("<[^>]*>", RegexOptions.Compiled);
3: string PostTextContent = _stripHTML.Replace(Post.Content, "");
4: int maxLength = 70;
5: string EncodedAbsoluteLink = Page.Server.UrlEncode(Post.AbsoluteLink.ToString());
6: string EncodedPostTitle = Page.Server.UrlEncode(Post.Title);
7: string EncodedPostBody = Page.Server.UrlEncode((PostTextContent.Length > maxLength) ? (PostTextContent.Substring(0, maxLength) + "...") : (PostTextContent));
8: string TagsQueryString = "";
9: foreach (BlogEngine.Core.Category cat in Post.Categories)
10: {
11: TagsQueryString += string.Format("&tags[]=" + Page.Server.UrlEncode(cat.Title));
12: }
13: %>
14: <IFRAME
15: width=60
16: height=55
17: marginWidth=0
18: marginHeight=0
19: frameBorder=0
20: scrolling=no
21: src="http://funp.com/tools/buttoniframe.php?url=<%=EncodedAbsoluteLink %>&s=1">
22: </IFRAME>
這是推文的部份,如果要張貼的話就不一樣了,要放的是把文章的預設資訊都帶過去,免的到時要重新輸入一次... 這部份的 CODE 比較囉唆,不過產生出來的 CODE 比較單純,就是個 <A> LINK 而以,不過因為帶的資訊比較多,所以部份 CODE 是由上面的 CODE 事先產生好,這裡才拿來用的:
1: <a href="http://funp.com/push/submit/add.php?url=<%=EncodedAbsoluteLink %>&s=<%=EncodedPostTitle %>&t=<%=EncodedPostBody %><%=TagsQueryString %>&via=tools" title="貼到funP">
2: <img src="http://funp.com/tools/images/post_03.gif" border="0"/>
3: </a>
果然效果好多了,也不會再碰到版面掛掉等等鳥問題,只不過載入 [封存] 頁面時,一次四五百個 <IFRAME> 同時在跑,IE也是跑的很吃力....
同樣的技巧也拿來修改 ~/archive.aspx 這頁。這頁原本是把所有的文章按照分類一篇一篇列出來,捨棄原有的 RATING 機制不用,直接用推文的機制取代。因此這頁原本顯示 RATING 分數的地方就被我改成推推王的推薦次術了。我的文章有兩百多篇,出現過的地方都列一次,加一加總共會出現近五百個推文按鈕 @_@,自然也不可能用原本官方的作法產生按鈕,直接用上面挖出來的方法,修改 archive.aspx.cs:
1: if (BlogSettings.Instance.EnableRating)
2: {
3: HtmlTableCell rating = new HtmlTableCell();
4: rating.InnerHtml = string.Format(
5: @"6: <IFRAME
7: marginWidth=0
8: marginHeight=0
9: src='http://funp.com/tools/buttoniframe.php?url={0}&s=12'
10: frameBorder=0
11: width=80
12: scrolling=no
13: height=15>
14: </IFRAME>",
15: (post.AbsoluteLink.ToString()));
16: rating.Attributes.Add("class", "rating");
17: row.Cells.Add(rating);
18: }
嗯,看起來效果好多了,至少我自己看起來順眼多了 :D
下一篇預告一下,下一篇會推出我自己寫的 PostViewCounter Extension,主要就是拿來作每篇文章的點閱率。BlogEngine 沒內建,找來的現成的又不是很合用,索性就自己寫了一個,請期待續篇 :D
沒什麼特別的,只是針對這次的盜文事件,我很不滿意百度 (Baidu.com) 的處理方式而以。
幾個月前曾發生過 BLOGGER.COM 有人直接把我的文章一字不漏的貼在他的 BLOG 上,沒有標示文章出處,最後用我破破的英文跟 GOOGLE 反映之後,GOOGLE 立即作了處理,關閉那位使用者的網頁。
這次碰到類似的情況,有耐心的人就聽我講完這無聊的故事吧。無意間我在對岸的入口網站 [百度知道] (類似奇摩知識+的網站),發現有人拿我的文章,一字不漏的貼上去回答問題賺點數,一樣沒有標示文章出處,感到非常的不滿,馬上註冊了帳號,留下回應表示該文侵犯了我的權益,要求引用文章要註明出處,同時也跟站方反應了這個情況,要求站方作妥善的處理。
原本以為事情會像上次一樣,跟 GOOGLE 一樣的處理方式結束。沒料到...
很無聊的戲就這樣一直演下去... 就是不斷的抗議又被刪除,跟站方反應卻又不理睬...。看來小蝦米是對抗不了大鯨魚的,也只能這樣了。其實我除了文章被盜貼之外沒有什麼具體的損失,就是心理很不爽而以,而更離譜的是百度站方處理的態度...。
資訊隨手可得,不代表資訊是可以任意踐踏的。免費的資訊,不用付費不代表就不需要尊重,也許對岸還有很多使用者沒建立起這樣的觀念,但是百度站方的處理方式也令我跌破眼鏡,有這樣的站方難怪會縱容這樣的使用者... :@
身為渺小不起眼 BLOG 主人,我也只能用消極的抗議,來表示我的不滿。除了寫這篇文章以外,也順帶來個 ASP.NET HttpModule 教學...。針對這次事件,我特地在本站加上了這個 HttpModule,只要查出使用者是透過任何由百度提供的 LINK 而連到本站的話,都會顯示這頁抗議的畫面,如下:
顯示了 60 秒抗議畫面後,就會自動進如原本要連結的頁面。在透過正規的管道而得不到妥善的處置,我也只能用消極的抗意來表達我的不滿。請看到的人留個 MESSAGE 支持一下吧,或是有推推王帳號的人也幫忙推一下,一起對不重視智慧財產權的百度表答不滿 & 抗議!
抗議之餘,本站再怎樣也是討論進階 .NET 技術的網站,就拿這次的案例,看看這樣的 HttpModule 該怎麼處理! 未來如果你也不幸碰到這樣的事件 (最好不要碰到),可以拿出來用一用! 要替網站加上這樣的功能很簡單,只要在 Web.config 把你寫的 HttpModule 掛上就好。一旦掛上,所有針對這個網站的 Http Request 都會經過你的 HttpModule 處理,任何一個 LINK 都跑不掉!
public class SiteBlockerHttpModule : IHttpModule { public void Init(HttpApplication context) { context.AuthenticateRequest += new EventHandler(context_AuthenticateRequest); } void context_AuthenticateRequest(object sender, EventArgs e) { HttpApplication application = sender as HttpApplication; string referer = application.Context.Request.ServerVariables["HTTP_REFERER"]; if (string.IsNullOrEmpty(referer) == false) { Uri refererURL = new Uri(referer); if (refererURL.Host.ToUpperInvariant().Contains("BAIDU.COM") == true) { application.Context.Server.Transfer("~/Blogs/ShowBlockedMessage.aspx"); } } } }
--
後記: 針對這次事件的記錄:
哈哈,終於加回來了 :D
為什麼原本在 CS 上很簡單就加上去的 Bot Checker, 在 BE 上弄到現在才好? 原因只有一個,就是 BE 在張貼回應時用了不少 AJAX 的機制,變的要插一段 CODE 進去要追半天 @_@...
很諷刺的是,AJAX 其實是 Community Server 用的比較兇,到處都要來一下... 反而 BlogEngine.NET 就中規中舉多了,很多地方就都乖乖的用 PostBack.. 唯讀回應的地方很突兀,感覺好像是特別要現一下回應的那個 TEXT EDITOR 還有 BBCODE 預覽的樣子... 那邊的 CODE 弄的實在是有點亂...
也是之前幾次都沒認真追啦... 追到一半覺的煩就去逛網站了,哈哈... 今天狠下心把它解決掉了。只不過卡著 AJAX,又不想跟它奮鬥,所以有些地方就偷懶混過去了。什麼意思? 意思就是攤開 HTML 原始碼,這個 Bot Checker 也是防不了 Bot 啦,不過我就賭我的站還有我的 Bot Checker 沒大到有人願意寫 Bot 來攻擊我.. 哈哈.. 來看看效果:
當然,張貼出去的回應也會帶著 Bot Checker 的問題。只不過礙於 AJAX,一堆東西被迫要移到 CLIENT SIDE 來處理,這邊就偷懶,題目產生完就先填到 [評論] 欄了,各位在填回應時,不爽附上 "芭樂雞萬歲" 的話,還是可以把它刪掉...
--
題外話,在追 BE 的程式碼的過程中,發現 BE 也有 CAPTCHA ? 不過還真的找不到怎麼把它打開... 有興趣用 BlogEngine.NET 又想用正統的 CAPTCHA 驗證的人可以試看看