راسلني قبل بضعة أسابيع أحد الزوار من مصر الحبيبة مستفسرا عن الطريقة التي طبقت بها سيناريو إدخال نص صورة التحقق (المزعجة)، وبما أنى أضعف كثيرا ولا أقاوم طلبات أي مبرمج مصري من أم الدنيا، فهذا المقال سيكون تلبية لرغبته وشرح تفصيلي يعرض له عملها تركي إزاي! إزاي .. إزاي .. إزاي .. اوصف لك يا حبيبي إزاي ... أبل ما أحبك كنت إزاي يا حبيبي .... كان هذا اقتباس من أحد صوتيات أم كلثوم، ولو تلاحظ في الاقتباس السابق أنها لمحت إلى وضعها (قبل) المحبة راسمة صورة ذات انطباع مأساوي لها، وهذا بالضبط وصف مقارب جدا للحالة المأساوية التي كنت بها (في موقعي السابق وليس لقصة من إلي في بالك)، والسبب كان من بعض الفضوليين (هداهم الله) الذين لا يطيقون رؤية اسمي ولا عمل ولا شغل لهم إلا التطفل على المواقع بأي طريقة تؤدي إلى تخريبه أو تعكير صفو صاحبه. وماذا فعلوا؟ دعني أحكي لك الحكاية من البداية: في نماذج ويب Web Forms يتم إرسال الصفحات من العميل إلى الخادم على نمط يسمى Post، ويتم (في اغلب الأحوال) بدء عملية الإرسال بالضغط على زر Submit، ولكن المشكلة أن لغات الصفحات الديناميكية DHTML (كـ JavaScript و VBScript) يمكن لها أن ترسل البيانات (على نمط Post) دون اشتراط الضغط على زر Submit، يتم ذلك –بكل بساطة- باستدعاء خفيف إلى الطريقة Submit() التابعة للكائن Form (والمحضون داخل كائن المستند Document). من هنا يستطيع أي شخص مزعج جدا ولا يطيق رؤية اسمك، كتابة كود بسيط (لا يتعدى بضعة اسطر) يقوم بإجراء عملية الإرسال Post، والمصيبة أن هذا الشخص يتلذذ ويستمتع جدا عندما يضيف هذا الكود بين فكي حلقة تكرارية (for أو while) مما تسبب في إرسال مئات (إن لم يكن آلاف) البيانات على شكل Post تلقائيا، والتي تنتهي رحلتها -بأغلب الأحوال- في جدول من جداول قاعدة بيانات الموقع، مما يؤدي إلى زيادة حجم قاعدة البيانات (دون فائدة). وبالنهاية، ادفع الفاتورة على المساحة الزائدة والحسابة بتحسب! لهذا السبب (وغيرها من الأسباب التي لا أود التوغل فيها) أصبحت الموضة عند اغلب المواقع تصعيب عملية الإرسال التلقائي للبيانات Auto Posting، وذلك بوضع صورة تظهر أرقام أو حروف عشوائية في كل مرة يتم فيها إرسال بيانات لنموذج ويب Web Form إلى الخادم، ومن الجيد أن يظهر النص بصورة تصعب قراءته (ولو بشكل بسيط) حتى لا تقوم تطبيقات القارئ الآلي OCRs من فهم النص بالصورة، وإلا سترجع حليمة لعادتها القديمة! عندما طورت موقعي البسيط al-asiri.COM، أردت تطبيق هذا السيناريو ليظهر حروف عربية، ولكني لم أجد (حتى هذه اللحظة) أي موقع عربي يطبق لغته الأم، وعندما حاولت الحصول على كود جاهز من المواقع الأجنبية، وجدته معقدا جدا ويتطلب الاتصال بقاعدة بيانات، لذلك قررت أن أقوم ببنائه من الصفر فهو أمر بسيط ولا يستحق كل هذا البحث (لن أقول سخيف وتافه عشان لا يزعلون بعض الناس)، فكانت أول خطوة لي (بدون تفكير) بناء أداة مستخدم User Control وأسميتها TransactionAuth: قمت بعدها بمسك فأرتي وأخذتها إلى صندوق الأدوات ToolBox، وأضفت أداتي Text و Image، وصممتهما بهذا الشكل: وعندما اتجهت إلى خانة التحرير، وجدت بيئة التطوير Visual Studio قد ولدت –مشكورة- هذه الشيفرة: Basic: <%@ Control Language="VB" AutoEventWireup="false" CodeFile="TransactionAuth.ascx.vb" Inherits="TransactionAuth" %> <p> <asp:Image ID="imgTransactionAuth" runat="server" height="50px" width="150px" style="border: solid 1px #000000"/><br /><br /> اكتب النص الظاهر في صورة التحقق: <asp:TextBox ID="txtTransactionAuth" runat="server" Width="100px"></asp:TextBox> </p> C#: <%@ Control Language="C#" AutoEventWireup="true" CodeFile="TransactionAuth.ascx.cs" Inherits="TransactionAuth" %> <p> <asp:Image ID="imgTransactionAuth" runat="server" height="50px" width="150px" style="border: solid 1px #000000"/><br /><br /> اكتب النص الظاهر في صورة التحقق: <asp:TextBox ID="txtTransactionAuth" runat="server" Width="100px"></asp:TextBox> </p> وبهذا أكون قد انتهيت من التصميم المرئي Visual Design للأداة، والآن حان وقت الجد. شرح السيناريو قبل التوغل في الشيفرة المصدرية، لابد من معرفة ماذا سأفعل وكيف ستكون الأوضاع والحياة المنطقية لمشروعنا، السيناريو المتبع بسيط جدا، وفكرته تتمحور على قيمتين: الأولى تمثل مفتاح Key أستخدمه كمعرف ID لعملية إدخال جديدة لنموذج ويب، والقيمة الثانية تمثل كلمة Word وهو النص المطلوب إدخاله من الزائر. وسيتم حفظ هذه القيم في مكان (نسميه جدول المفاتيح والكلمات). وحتى نرى كيف يحدث هذا وذاك، لنتبع خطوات هذا السيناريو: 1. دخل عباس السريع أحد الصفحات التي تعرض نموذج ويب. 2. سنقوم فورا (وقبل ظهور الصفحة) بتعريف مفتاح جديد Key بقيمة عشوائية Random (ليكن 123). 3. نطلب تسجيل المفتاح الجديد في جدول المفاتيح والكلمات ونعرف كلمة مرافقه له بشكل عشوائي (لتكن "نمقس"). 4. في هذه الأثناء، قام برعي أبو جبهة بزيارة الصفحة، ومثل ما عملت لعباس السريع من قبل، سأقوم بعمل نفس الشيء لبرعي ابو جبهة، ونسجل قيمة لمفتاح وكلمة عشوائيتين جديدتين: 5. ظهرت صفحة النموذج لعباس السريع، أحتاج الآن إلى تعليم الصفحة وربطها بالجدول، ويمكنني عمل ذلك بإظهار الكلمة في صورة (من المهم أن تكون في صورة حتى لا يستطيع فهمها إلا عقل بشري): 6. ونفس الشيء مع برعي ابو جبهة، سيظهر له النموذج بالصورة، ولكن كيف نظهر الكلمة المناسبة له في الصورة، فلا نريد أن نخلطها بالكلمة التي ظهرت لعباس؟ ولا تنسى أني بحاجة ماسة جدا جدا (أكثر من حاجة الرضيع إلى حليب أمه) إلى رقم المفتاح key، وذلك حتى أتمكن من إجراء المقارنة بين القيمة التي ادخلها والقيمة في جدول المفاتيح والكلمات (بعد إرسال البيانات إلى الخادم). بكل تأكيد علينا أن نستخدم المفتاح key الذي يميز صفحة عباس عن صفحة برعي، وتوجد 3 طرق لعمل ذلك، الأولى هي الطريقة الغبية جدا، والتي أطلب فيها من الزائر كتابة رقم المفتاح الذي سجلناه (عند بداية زيارة الصفحة): الطريقة الثانية هي الأقل غباء، والتي تقتضي ان أقوم بكتابة المفتاح بدلا من الزائر: وما دخل الزائر برقم مفتاحه؟ فالزائر لا يعلم أي شيء عن جدول المفاتيح والكلمات، لذلك تكون الطريقة الثالثة هي الاختيار الصحيح وهي (عدم) إظهار رقم المفتاح وجعله مخفي (عن أعين الزائر فقط)، يمكننا حفظه –مثلا- في أي وسم من وسوم الأداة: <div tranId=”322” …> … … </div> 7. أخيرا، بعد أن يقوم الزائر بالضغط على زر Submit، سنقوم بالبحث عن رقم المفتاح في جدول المفاتيح والكلمات، ونقارن الكلمة التي ادخلها بالكلمة الموجودة في الجدول. من الضروري جدا جدا جدا حذف السجل من جدول المفاتيح والكلمات بعد كل عملية اختبار عليه (سواء كان الاختبار صحيحا أم خاطئا)، والسبب لا يقتصر فقط على أن السجلات ستكثر بشكل خرافي في الجدول، بل المصيبة تتعدى هذا الأمر، إذ إن الزائر لا يزال يستطيع الاعتماد على (نفس) قيمة المفتاح ويعيد استخدامه في كل عملية إرسال البيانات، وترجع حليمة لعادتها القديمة! شيفرة الأداة TransactionAuth أتمنى –من صميم قلبي- أن تكون قد اتضحت لك فكرة السيناريو الذي سأطبقه، وألان ليس لدي سوى عرض ابرز الاكواد المستخدمة، بالنسبة للأداة TransactionAuth، فأضفت في حدث التحميل Load كود لاستدعاء الطريقة الخاصة ShowNewPic()، وذلك في كل مرة يتم فتح الصفحة لأول مرة فقط (بسبب الشرط Me.IsPostBack أو !this.IsPostBack): Basic: Partial Class TransactionAuth Inherits System.Web.UI.UserControl ... ... Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load If Not Me.IsPostBack Then Me.ShowNewPic() End Sub ... ... End Class C#: public partial class TransactionAuth : System.Web.UI.UserControl { … … protected void Page_Load(object sender, EventArgs e) { if (!this.IsPostBack) this.ShowNewPic (); } … … } الطريقة ShowNewPic() تحاول توليد مفتاح عشوائي جديد، وتقوم بحفظه داخل وسم <img> والخاص بالصورة، وبعد ذلك تقوم بتسجيل المفتاح في جدول المفاتيح والكلمات باستدعاء قوي للطريقة RegisterKey(): Basic: Private Sub ShowNewPic() ' تسجيل مفتاح ' واظهار صورة لنص جديد If Me.imgTransactionAuth.Attributes("tranID") Is Nothing Then Me.imgTransactionAuth.Attributes.Add("tranID", Me.GenerateNewKey()) Else Me.imgTransactionAuth.Attributes("tranID") = Me.GenerateNewKey() End If TransactionAuth.RegisterKey(Me.imgTransactionAuth.Attributes("tranID")) Me.imgTransactionAuth.ImageUrl = "GetImage.aspx?ShowTransactionAuth=" & _ Me.imgTransactionAuth.Attributes("tranID") End Sub C#: private void ShowNewPic() { // تسجيل مفتاح // واظهار صورة لنص جديد if (this.imgTransactionAuth.Attributes["tranID"] == null) this.imgTransactionAuth.Attributes.Add("tranID", this.GenerateNewKey()); else this.imgTransactionAuth.Attributes["tranID"] = this.GenerateNewKey(); TransactionAuth.RegisterKey(this.imgTransactionAuth.Attributes["tranID"]); this.imgTransactionAuth.ImageUrl = "GetImage.aspx?ShowTransactionAuth=" + this.imgTransactionAuth.Attributes["tranID"]; } وعلى ذكر جدول الكلمات والمفاتيح، فهو في الحقيقة ليس جدول بقاعدة بيانات، وإنما مجرد مصفوفة (لا راحت ولا جت) من النوع ArrayList: Basic: ' مصفوفة السجلات Private Shared keyWordRecordArr As New ArrayList C#: // مصفوفة السجلات private static ArrayList keyWordRecordArr = new ArrayList(); المصفوفة keyWordRecordArr مستهدفة من قبل جماعات الطرق التابعة للأداة ليتم إضافة عناصر تمثل السجلات لكائنات من النوع keyWordRecord: Basic: ' الفئة الخاصة بحفظ سجلات الارقام العشوائية والمفاتيح Private Class keyWordRecord Friend key As String Friend word As String Friend t As Date Sub New(ByVal k As String, ByVal w As String, ByVal t As Date) key = k word = w t = t End Sub End Class C#: // الفئة الخاصة بحفظ سجلات الأرقام العشوائية والمفاتيح class keyWordRecord { internal String key; internal String word; internal DateTime t; internal keyWordRecord (String k, String w, DateTime t) { this.key = k; this.word = w; this.t = t; } } من المهم جدا الاحتفاظ بقيمة من النوع DateTime تمثل الوقت الذي تم تسجيل المفتاح فيه، والسبب ان الزائر قد يغادر الصفحة ولا يقوم بعمل Submit، ولذلك سأحتفظ بهذا الوقت حتى أقوم بحذف السجل لاحقا إن تعدى فترة دقائق معينة ولم يستخدم (نفس فكرة Timeout Period). أخيرا، ابرز طرق الأداة هي الطريقة CheckTransactionAuth()، وبالرغم من أن طولها وشكلها مخيف إلا أنها كلام فاضي، فالهدف منها القيام بعملية التحقق من توافق الكلمة التي ادخلها الزائر مع الكلمة الموجودة في مصفوفة (جدول) المفاتيح والكلمات استنادا إلى المفتاح المرسل لها: Basic: Private Shared Function CheckTransactionAuth(ByVal transactionID As String, ByVal inputText As String) As Boolean ' تقوم هذه الدالة بالتحقق من النص المدخل ' وهل يوافق النص الظاهر على الصورة If transactionID.Trim() = String.Empty OrElse inputText.Trim() = String.Empty Then Throw New Exception(EX_WRONG_TRANSACTION_AUTH) Else Dim i As Integer = 0 Do While i <= keyWordRecordArr.Count - 1 If CType(keyWordRecordArr(i), keyWordRecord).key = transactionID Then If CType(keyWordRecordArr(i), keyWordRecord).word = inputText Then keyWordRecordArr.RemoveAt(i) Return True Else keyWordRecordArr.RemoveAt(i) Throw New Exception(EX_WRONG_TRANSACTION_AUTH) End If ElseIf CType(keyWordRecordArr(i), keyWordRecord).t.AddMinutes(2) < Now Then keyWordRecordArr.RemoveAt(i) Else i = i + 1 End If Loop Throw New Exception(EX_WRONG_TRANSACTION_AUTH) End If End Function C#: private static Boolean CheckTransactionAuth(string transactionID, string inputText) { // تقوم هذه الدالة بالتحقق من النص المدخل // وهل يوافق النص الظاهر على الصورة if (transactionID.Trim() == string.Empty || inputText.Trim() == string.Empty) throw new Exception(EX_WRONG_TRANSACTION_AUTH); else { int i = 0; while ( i <= keyWordRecordArr.Count - 1 ) if ( ((keyWordRecord) keyWordRecordArr[i]).key == transactionID ) if ( ((keyWordRecord) keyWordRecordArr[i]).word == inputText ) { keyWordRecordArr.RemoveAt(i); return true; } else { keyWordRecordArr.RemoveAt(i); throw new Exception(EX_WRONG_TRANSACTION_AUTH); } else if ( ((keyWordRecord) keyWordRecordArr[i]).t.AddMinutes(2) < DateTime.Now ) keyWordRecordArr.RemoveAt(i); else i = i + 1; throw new Exception(EX_WRONG_TRANSACTION_AUTH); } } برمجيا، كل ما تقوم به هذه الطريقة عبارة عن حلقة تكرارية تتصارع فيها مع المصفوفة keyWordRecordArr وتقوم برمي استثناء Throw Exception في حالة عدم توافق القيم. الجدير بالذكر هنا، بأن الطريقة ستقوم بحذف العنصر من المصفوفة سواء تحقق الشرط أم لا أو حتى إن مرت اكثر من دقيقتين على وجود السجل في المصفوفة. الصفحة GetImage.aspx الغرض الرئيسي من الصفحة GetImage.aspx (أفضل أن اسميها ملف في هذا السيناريو) هو التوليد الديناميكي للصورة Dynamic Picture Generating، والمقصد (هيا أخبرني أنت) رسم الصورة (الكلمة) التي ستظهر للزائر التي نود أن يكتبها للتحقق. سيقودك فضولك البرمجي إلى فتح الملف GetImage.aspx وستتفاجئ بأنه ملف خالي (يعني فاضي وليس أخو أمي) ولا يوجد به سوى سطرين: Basic: <%@ Page Language="VB" AutoEventWireup="false" CodeFile="GetImage.aspx.vb" Inherits="GetImage" %> <%@ Register Src="TransactionAuth.ascx" TagName="TransactionAuth" TagPrefix="uc1" %> C#: <%@ Page Language="C#" AutoEventWireup="true" CodeFile="GetImage.aspx.cs" Inherits="GetImage" %> <%@ Register Src="TransactionAuth.ascx" TagName="TransactionAuth" TagPrefix="uc1" %> بالنسبة للسطر الثاني، فوجوده ضروري لأني بحاجة إلى استدعاء أحد طرق الأداة TransactionAuth وهي الطريقة GetWorkByKey() والتي تعود بالكلمة استنادا إلى المفتاح المرسل. اما ان كشفنا المستور وبحثنا في الشيفرة التي تحرك الملف GetImage.aspx، فسنرى الطريقة الخاصة showTransactionAuth() والتي تقوم باللعب مع فئات GDI+ لرسم الصورة: Basic: Private Sub showTransactionAuth() Dim bmp As New Drawing.Bitmap(150, 50, Drawing.Imaging.PixelFormat.Format16bppRgb565) Dim gr As Drawing.Graphics = Drawing.Graphics.FromImage(bmp) Dim Font As New Drawing.Font("Tahoma", 16) Dim text As String = TransactionAuth.GetWordByKey(Me.Request.QueryString("ShowTransactionAuth")) gr.ResetTransform() gr.TranslateTransform(20, 1) gr.RotateTransform(New Random().Next(10, 15)) gr.DrawString(text, Font, Drawing.Brushes.White, 0, 0) Response.ContentType = "image/jpeg" bmp.Save(Response.OutputStream, Drawing.Imaging.ImageFormat.Jpeg) Font.Dispose() gr.Dispose() bmp.Dispose() End Sub C#: private void showTransactionAuth() { Bitmap bmp = new Bitmap(150, 50, System.Drawing.Imaging.PixelFormat.Format16bppRgb565); Graphics gr = Graphics.FromImage(bmp); Font Font = new Font("Tahoma", 16); string text = TransactionAuth.GetWordByKey(this.Request.QueryString["ShowTransactionAuth"]); gr.ResetTransform(); gr.TranslateTransform(20, 1); gr.RotateTransform(new Random().Next(10, 15)); gr.DrawString(text, Font, Brushes.White, 0, 0); Response.ContentType = "image/jpeg"; bmp.Save(Response.OutputStream, System.Drawing.Imaging.ImageFormat.Jpeg); Font.Dispose(); gr.Dispose(); bmp.Dispose(); } أبرز ما أذكره هنا عن السطر Response.ContentType، الذي يحول التوجه الفلسفي لصفحة ASP.NET ويغير مخرجاتها من مخرجات HTML تقليدية ( تعنون بـ text/html) إلى مخرجات من نوع صورة ( تعنون بـ image/jpeg ) يفهمها المستعرض Browser. استخدام الأداة حتى وان لم تهتم بالخرابيط المذكورة في هذا المقال، فأكثر ما يهمك هنا هو طريقة الاستفادة من الأداة واستخدامها(والذي لا يتطلب منك أي مجهود). فيكفي ان تضع الأداة في المكان الذي تريده من الصفحة، وعندما تنوي البدء في عملية التحقق، كل ما تحتاجه سطر واحد فقط يستدعي الطريقة CheckTransactionAuth()، وأكمل بعده مشوار حياتك كأن شيئا لم يحدث: Basic: Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click Try Me.TransactionAuth1.CheckTransactionAuth() ' ... ' ... ' اكمل مشوار حياتك كما تشاء ' ... Me.lblException.Text = "النص المدخل صحيح" Catch ex As Exception Me.lblException.Text = ex.Message End Try End Sub C#: protected void Button1_Click(object sender, EventArgs e) { try { this.TransactionAuth1.CheckTransactionAuth(); // ... // ... // اكمل مشوار حياتك كما تشاء // ... this.lblException.Text = "النص المدخل صحيح"; } catch (Exception ex) { this.lblException.Text = ex.Message; } } الشيفرة السابقة تعتبر الاستخدام النموذجي للأداة TransactionAuth، وتلاحظ أني قمت بتدارك استثناء Handling an Exception والسبب ان الطريقة CheckTransactionAuth() سترمي استثناء صاعق جدا في حال ما ان قام المستخدم بإدخال رقم غير متوافق مع صورة التحقق. خاتمة حاولت في هذا المقال شرح تطبيق سيناريو "أدخل نص صورة التحقق" برمجيا استجابة لطلب أحد الزوار، وان كنت يا ايها السائل قد وجدته معقدا وكان شرحي غير واضح، فهذا ابسط ما أستطيع تقديمه لك. مع ذلك، يمكنك الحصول على الأداة بلغتي C# و VB جاهزة للذبح، وتستطيع إضافتها في مشاريعك وجميع نماذج ويب التي تستخدمها. -- تركي