نظرة خاطفة على وحدة التحكم في ريلز

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث

ستتعرف في هذا الدليل على كيفية عمل وحدات التحكم (Controller) وكيفية ملاءمتها مع دورة الطلب (request cycle) في التطبيق الخاص بك.

بعد قراءة هذا الدليل، ستتعلم:

  • كيفية اتباع تدفق الطلب من خلال وحدة تحكم.
  • كيفية تقييد المعاملات التي مُرّرت إلى وحدة التحكم الخاصة بك.
  • كيف ولماذا تخزَّن البيانات في الجلسة (session) أو ملفات تعريف الارتباط (cookies).
  • كيفية العمل مع المرشحات (filters) لتنفيذ التعليمات البرمجية أثناء معالجة الطلب.
  • كيفية استخدام استيثاق HTTP المضمن في وحدة التحكم.
  • كيفية تدفق البيانات مباشرة إلى متصفح المستخدم.
  • كيفية ترشيح المعاملات الحساسة بحيث لا تظهر في سجل التطبيق.
  • كيفية التعامل مع الاستثناءات التي قد تنشأ أثناء معالجة الطلب.

ما الذي تفعله وحدة التحكم؟

وحدة التحكم Action Controller هي الحرف C في نمط التصميم MVC (اختصار للعبارة Model View Controller). بعد أن يحدِّد جهاز التوجيه (router) أي وحدة تحكم تُستخدَم لطلب ما، تكون وحدة التحكم مسؤولة عن فهم الطلب وإنتاج الناتج المناسب.

لحسن الحظ، تقوم وحدة التحكم Action Controller بمعظم المهام الأساسية الخاصة بك ويستخدم المواثيق الذكية (smart conventions) لجعل هذا الأمر بسيطًا قدر الإمكان.

بالنسبة لمعظم تطبيقات RESTful التقليدية، ستتلقى وحدة التحكم الطلب (وهذا غير مرئي بالنسبة لك كمطور)، وتجلب البيانات أو تحفظها من النموذج وتستخدم الواجهة لإنشاء مخرجات HTML. إذا كانت وحدة التحكم بحاجة إلى عمل الأشياء بطريقة مختلفة قليلًا، فهذه ليست مشكلة؛ هذه فقط الطريقة الأكثر شيوعًا لتعمل وحدة التحكم.

وبالتالي، يمكن اعتبار وحدة التحكم كوسيط بين النماذج والواجهات. فهي تجعل بيانات النموذج متاحة للعرض حتى تمكنه من عرض تلك البيانات للمستخدم، وتحفظ أو تُحدِّث بيانات المستخدم إلى النموذج.

لمزيد من التفاصيل حول عملية التوجيه، راجع صفحة التوجيه من الخارج إلى الداخل.

اتفاقية التسمية في وحدة التحكم

تفضل اتفاقية التسمية لوحدات التحكم في ريلز صيغة الجمع  للكلمة الأخيرة في اسم وحدة التحكم، على الرغم من أنه ليس مطلوبًا تمامًا (مثل ApplicationController).

على سبيل المثال، ClientsController أفضل من ClientController و SiteAdminsController أفضل من SiteAdminController أو SitesAdminsController، وهكذا دواليك.

تسمح لك اتباع هذه الاتفاقية باستخدام مولدات المسارات الافتراضية (مثل resources ...إلخ.) دون الحاجة إلى تأهيل كل من المسار ‎:path أو وحدة التحكم ‎:controller، وسيبقى استخدام العنوان URL ودوال المسار المساعدة مترابطًا في جميع أنحاء التطبيق الخاص بك.

راجع دليل التخطيطات والتصيير لمزيد من التفاصيل.

ملاحظة: تختلف اتفاقية تسمية وحدة التحكم من اتفاقية تسمية النماذج، والتي من المتوقع أن تُسمّى بصيغة المفرد.

التوابع والإجراءات

وحدة التحكم هي صنف من أًصناف روبي ترث من ApplicationController ولها توابع مثل أي صنف آخر. عندما يتلقى تطبيقك طلبًا، سيحدد التوجيه وحدة التحكم والإجراء المطلوب تشغيله، ثم تنشئ ريلز نسخةً لوحدة التحكم هذه ويشغل التابع الذي يحمل نفس اسم الإجراء.

class ClientsController < ApplicationController
  def new
  end
end

على سبيل المثال، إذا انتقل المستخدم إلى ‎/clients/new في تطبيقك، لإضافة عميل جديد، ستنشئ ريلز نسخةً من ClientsController وسيستدعي تابعًا جديدًا. لاحظ أن التابع الفارغ من المثال أعلاه سيعمل على ما يرام لأن ريلز افتراضيًا تقدم واجهة new.html.erb ما لم ينص الإجراء على خلاف ذلك. يمكن أن يتيح التابع الجديد واجهة متغير نسخة client@ عن طريق إنشاء عميل جديد:

def new
  @client = Client.new
end

يوضح دليل التخطيطات والتصيير هذا الأمر بمزيد من التفصيل.

يرث ApplicationController من ActionController::Base، الذي يحدد عددًا من التوابع المفيدة. سيغطي هذا الدليل بعضًا من هذه الأمور، ولكن إذا كنت مهتمًا برؤية ما هناك، فيمكنك الاطلاع عليها جميعًا في توثيق واجهة برمجة التطبيقات عبر هذا الرابط.

فقط التوابع العامة هي قابلة للاستدعاء كإجراء. من أفضل الممارسات تقليل رؤية التوابع (مع الخصوصية أو الحماية) التي لا يُقصَد بها أن تكون إجراءات، مثل التوابع المساعدة أو المرشحات.

المعاملات

ربما تريد الوصول إلى البيانات المرسلة بواسطة المستخدم أو المعاملات الأخرى في وحدة التحكم الخاصة بك. هناك نوعان من المعاملات الممكنة في تطبيق الويب: الأولى هي المعاملات التي ترسل كجزء من عنوان URL، وتسمى معاملات سلسلة الاستعلام. سلسلة الاستعلام هي كل شيء بعد الرمز "?" في عنوان URL. عادة ما يشار إلى النوع الثاني من المعاملات ببيانات POST.

عادةً ما تأتي هذه المعلومات من نموذج HTML ملئ بواسطة المستخدم. يطلق عليها بيانات POST لأنه لا يمكن إرسالها إلا كجزء من طلب HTTP POST. لا تميز ريلز بين معاملات سلسلة الاستعلام ومعاملات POST، وكلاهما متاح في params التي من النوع Hash في وحدة التحكم الخاصة بك:

class ClientsController < ApplicationController
  # HTTP GET يستخدم هذا الإجراء معاملات سلسلة الاستعلام لأنه يشغل بواسطة طلب 
  # ولكن هذا لا يحدث أي فرق في الطريقة التي يصل بها إلى المعاملات. سيبدو 
  # لهذا الإجراء على هذا النحو من أجل قائمة العملاء المفعلة URL عنوان
  # /clients?status=activated
  def index
    if params[:status] == "activated"
      @clients = Client.activated
    else
      @clients = Client.inactivated
    end
  end
 
  # HTML هي على الأغلب قادمة من نموذج .POST يستخدم هذا الإجراء معاملات
  # "/clients" هو RESTful لطلب URL الذي أرسله المستخدم. سيكون عنوان 
  # الطلب (body) وترسل البيانات كجزء من نص 

  def create
    @client = Client.new(params[:client])
    if @client.save
      redirect_to @client
    else
      # يستبدل هذا السطر سلوك التصيير الافتراضي، والذي كان من شأنه 
      # "create" تصيير الواجهة
      render "new"
    end
  end
end

المعاملات ذات النوع Array و Hash

لا تقتصر المعاملات params ذات النوع Hash على مفاتيح وقيم أحادية البعد. يمكن أن تحتوي على مصفوفات وجداول Hash متداخلة. لإرسال مجموعة من القيم، ألحق زوجًا فارغًا من الأقواس المعقوفة "[]" باسم المفتاح:

GET /clients?ids[]=1&ids[]=2&ids[]=3

ملاحظة: يشفر عنوان URL الفعلي في هذا المثال على أنه"‎/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3" لأن المحرف "[" و "]" غير مسموح به في عناوين URL. في معظم الأوقات، لا داعي للقلق حيال ذلك لأنَّ المتصفح يرمزها لك، وتفك ريلز ترميزها تلقائيًا؛ ولكن، إذا وجدت نفسك مضطرًّا لإرسال تلك الطلبات إلى الخادم يدويًا، يجب أن تضع ذلك بالحسبان.

ستكون قيمة [params[:ids الآن ["1" ، "2" ، "3"]. لاحظ أن قيم المعاملات هي دائما سلاسل نصيّة؛ لا تحاول ريلز تخمين أو  تحويل النوع.

ملاحظة: تستبدل القيم مثل [nil] أو [nil, nil, ...‎] في params بالقوسين المعقوفين [] لأسباب أمنية بشكل افتراضي. انظر دليل الأمن لمزيد من المعلومات.

لإرسال قيمة من النوع hash، عليك تضمين اسم المفتاح داخل الأقواس:

<form accept-charset="UTF-8" action="/clients" method="post">
  <input type="text" name="client[name]" value="Acme" />
  <input type="text" name="client[phone]" value="12345" />
  <input type="text" name="client[address][postcode]" value="12345" />
  <input type="text" name="client[address][city]" value="Carrot City" />
</form>

عند إرسال هذا النموذج، ستكون قيمة المعاملات [‎:client] هي:

{ "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City"‎ } }

لاحظ التجزئة المتداخلة في المعاملات [‎:client][:address].

يعمل الكائن params مثل Hash ، ولكن يتيح لك استخدام الرموز والسلاسل النصية بالتبادل كمفاتيح.

معاملات JSON

إذا كنت تكتب تطبيقًا لخدمة ويب، فقد تجد نفسك أكثر راحة في قبول المعاملات بتنسيق JSON. إذا تعين ترويسة الطلب "Content-Type" من طلبك إلى "application/json"، تحمّل ريلز تلقائيًا المعاملات في params من النوع Hash، والتي يمكنك الوصول إليها كما تفعل عادةً.

على سبيل المثال، إذا كنت ترسل محتوى JSON التالي:

{ "company": { "name": "acme", "address": "123 Carrot Street" } }

ستتلقى وحدة التحكم الخاصة بك [params[:company بالشكل { "name" => "acme", "address" => "123 Carrot Street"‎}. أيضًا، إذا شغلت config.wrap_parameters في مُهيئك (initializer) أو استدعيت wrap_parameters في وحدة التحكم الخاصة بك، فيمكنك بأمان حذف العنصر الجذر (‎(root element في المعامل JSON. في هذه الحالة، ستستنسخ المعاملات وتغلف بمفتاح يحدد استنادًا إلى اسم وحدة التحكم الخاصة بك. لذلك يمكن كتابة طلب JSON أعلاه على النحو التالي:

{ "name": "acme", "address": "123 Carrot Street" }

وبافتراض أنك ترسل البيانات إلى CorporateController، فستغلف في المفتاح ‎:company كما يلي:

{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }

يمكنك تخصيص اسم المفتاح أو معاملات معينة تريد تغليفها عن طريق الاطلاع على توثيق الواجهة البرمجية.

ملاحظة: استُخرِج دعم تحليل معاملات XML في جوهرة (gem) تسمى actionpack-xml_parser.

معاملات التوجيه

تحتوي المعاملات params التي من النوع Hash دومًا على مفاتيح وحدة التحكم ‎:controller والإجراء ‎:action، ولكن يجب استخدام التابعين control_name و action_name بدلًا من ذلك للوصول إلى هذه القيم. أي معاملات أخرى يحددها التوجيه، مثل ‎:id ستكون متوفرة أيضًا. على سبيل المثال، ضع في اعتبارك قائمة العملاء حيث يمكن أن تعرض القائمة عملاء نشطين أو غير نشطين. يمكننا إضافة مسار يلتقط معامل الحالة ‎:status في عنوان ‎‎"pretty" URL:

get '/clients/:status' => 'clients#index', foo: 'bar'

في هذه الحالة، عندما يفتح المستخدم العنوان URL/clients/active، سيتم تعيين المعاملات [‎:status] إلى "نشط" (active). عند استخدام هذا المسار، سيعين أيضًا المعاملات [‎:foo] إلى "bar"، كما لو مررت في سلسلة الاستعلام. ستتلقى وحدة التحكم أيضًا [params[:action بالشكل "index" و [params[:controller بالشكل "clients".

التابع default_url_options

يمكنك تعيين المعاملات الافتراضية العامة لإنشاء عنوان URL من خلال تحديد تابع يسمى default_url_options في وحدة التحكم الخاصة بك. يجب أن يعيد هذا التابع جدول hash مع الإعدادات الافتراضية المطلوبة، والتي يجب أن تكون مفاتيحها رموزًا بالشكل التالي:

class ApplicationController < ActionController::Base
  def default_url_options
    { locale: I18n.locale }
  end
end

تستخدم هذه الخيارات كنقطة بداية عند إنشاء عناوين URL، لذلك من الممكن أن تتجاوز من خلال الخيارات التي مُرِّرت إلى url_for للمكالمات.

إذا قمت بتعريف default_url_options في ApplicationController، كما هو موضح في المثال أعلاه، فستُستخدَم هذه الإعدادات الافتراضية لجميع عمليات إنشاء عناوين URL. يمكن تعريف الطريقة أيضًا في وحدة تحكم معينة، وفي هذه الحالة لا تؤثر سوى على عناوين URL التي أنشئت هناك.

في طلب معين معطى، لا يُستدعَى التابع فعليًا لكل عنوان URL أُنشِئ؛ لأسباب الأداء، يخزَّن الجدول hash المعاد في ذاكرة التخزين المؤقتة الداخلية، إذ يوجد استدعاء واحد على الأكثر لكل طلب.

المعاملات القوية

مع استخدام المعاملات القوية (strong parameters)، يُحظَر استخدام معاملات وحدة التحكم في التعيينات الشاملة للنموذج النشط حتى تدرج في القائمة البيضاء. وهذا يعني أنه سيتعين عليك اتخاذ قرار واعي بشأن الخاصيات (attributes) التي تسمح بإجراء التحديث الشامل. هذه ممارسة أمان أفضل للمساعدة في منع المستخدمين عن طريق الخطأ من تحديث سمات الطرازات الحساسة.

بالإضافة إلى ذلك، يمكن وضع علامة على المعاملات على النحو المطلوب، وستتدفق من خلال تدفق اطلاق/إنقاذ (raise/rescue) محدَّد مسبقًا، مما يؤدي إلى إعادة الخطأ 400 للطلب إذا لم تمرر جميع المعاملات المطلوبة.

class PeopleController < ActionController::Base
  # ActiveModel::ForbiddenAttributesError سيؤدي هذا إلى اطلاق الاستثناء  
  # نظرًا لأنه يستخدم تعيينًا شامًلا بدون خطوة تصريح صريحة.
  def create
    Person.create(params[:person])
  end
 
  # person طالما هناك المفتاح (flying colors) سوف يمر هذا مع الألوان الطائرة 
  # ActionController::ParameterMissing في المعاملات، وإلا فإنه سيطلق الاستثناء 
  # ويحول إلى الخطأ 400 في ActionController::Base، والذي سيقبض عليه من قبل
  # .الطلب
  def update
    person = current_account.people.find(params[:id])
    person.update!(person_params)
    redirect_to person
  end
 
  private
    # لتغليف المعاملات المسموح بها هو مجرد نموذج private إن استخدام التابع
    # جيد حيث ستتمكن من إعادة استخدام نفس قائمة التصاريح بين الإنشاء
    # والتحديث. يمكنك تخصيص هذه الطريقة مع التحقق من كل مستخدم للخاصيات
    # .المسموح بها
    def person_params
      params.require(:person).permit(:name, :age)
    end
end

القيم العددية المسموح بها

إن اعطاء

params.permit(:id)

المفتاح ‎:id سيمرر القائمة البيضاء إذا ظهرت في المعاملات وكان لها قيمة قياسية مسموح بها. خلاف ذلك، سيرشح المفتاح، لذلك لا يمكن حقن المصفوفات أو القيم hash أو أي كائنات أخرى.

الأنواع القياسية المسموح بها هي String و Symbol و NilClass و Numeric و TrueClass و FalseClass و Date و Time و DateTime و StringIO و IO و ActionDispatch::Http::UploadedFile و Rack::Test::UploadedFile.

لتوضيح أن القيمة في params يجب أن تكون مصفوفة من القيم العددية المسموح بها، قم بتعيين المفتاح إلى مصفوفة فارغة:

params.permit(id: [])

في بعض الأحيان، لا يكون من الممكن أو الملائم الإعلان عن المفاتيح الصالحة لمعامل hash أو هيكلها الداخلي. ما عليك سوى التعيين إلى جدول hash فارغ:

params.permit(preferences: {})

لكن كن حذرًا لأن هذا يفتح الباب أمام الإدخال التعسفي. في هذه الحالة، يضمن التصريح أن القيم في البنية المعادة مسموح بها كإصلاحات ومرشحات من أي شيء آخر. لإدراج كل محتوى جدول hash من المعاملات إلى القائمة البيضاء، يمكن استخدام التابع permit!‎ على النحو التالي:

params.require(:log_entry).permit!

تدل ‎:log_entry على معاملات hash وأية hash فرعية لها كما هو مسموح به ولا يتحقق من القياسات (scalars) المسموح بها، إذ يقبل أي شيء. يجب توخي الحذر الشديد عند استخدام permit!‎، حيث أنه سيسمح بتعيين شامل لجميع خصائص النموذج الحالية والمستقبلية.

المعاملات المتداخلة

يمكنك أيضًا استخدام التابع permit على المعاملات المتداخلة، مثل:

params.permit(:name, { emails: [] },
              friends: [ :name,
                         { family: [ :name ], hobbies: [] }])

يضيف هذا الإعلان إلى القائمة خاصيات الاسم (name) والبريد الإلكتروني (emails) والأصدقاء (friends) ...إلخ. من المتوقع أن تكون عناوين البريد الإلكتروني عبارة عن مصفوفة من القيم العددية المسموح بها، وأن يكون الأصدقاء مصفوفة من الموارد ذات خاصيات (attributes) محددة، إذ يجب أن يكون لها الخاصية name (أية قيم قياسية مسموح بها)، والخاصية hobbies كمصفوفة من القيم العددية المسموح بها، والخاصية family التي تقتصر على وجود اسم (أي القيم العددية المسموح بها يمكن استعمالها هنا أيضًا).

مزيد من الأمثلة

قد ترغب أيضًا في استخدام الخاصيات المسموح بها في الإجراء الجديد. هذا يثير المشكلة التي لا يمكنك استخدام require على مفتاح الجذر لأنه، عادةً، لا وجود لها عند استدعاء new:

# يمكنك توفير قيمة افتراضية واستخدام معاملات ،`fetch` باستخدام
# واجهة برمجية قوية من هناك
params.fetch(:blog, {}).permit(:title, :author)

يسمح لك تابع صنف النموذج accepts_nested_attributes_for بتحديث السجلات المرتبطة وتدميرها. يعتمد هذا على المعاملات id و ‎_destroy:

# permit :id and :_destroy
params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])

تُعامَل الكائنات Hash التي مفاتيحها هي أعداد صحيحة بشكل مختلف، ويمكنك الإعلان عن الخاصيات كما لو كانت أبناءً مباشرين. يمكنك الحصول على هذه الأنواع من المعاملات عند استخدامك accepts_nested_attributes_for بالاقتران مع has_many المقابل:

# :لإضافة البيانات التالية إلى القائمة البيضاء
# {"book" => {"title" => "Some Book",
#             "chapters_attributes" => { "1" => {"title" => "First Chapter"},
#                                        "2" => {"title" => "Second Chapter"}}}}
 
params.require(:book).permit(:title, chapters_attributes: [:title])

خارج نطاق المعاملات القوية

صُمِّمت واجهة برمجة التطبيقات (API) للمعامل القوي مع أخذ حالات الاستخدام الأكثر شيوعًا في الحسبان. لا يقصد به العصا السحرية التي تعالج جميع مشاكل القائمة البيضاء. ومع ذلك، يمكنك بسهولة خلط الواجهة البرمجية مع التعليمات البرمجية الخاصة بك للتكيف مع الموقف الخاص بك.

تخيل السيناريو التالي: لديك معاملات تمثل اسم منتج وجدول hash من البيانات التعسفية المرتبطة بهذا المنتج، وتريد إدراج خاصية اسم المنتج في القائمة البيضاء وكذلك تجزئة البيانات بالكامل. لا تسمح لك واجهة برمجة التطبيقات للمعاملات القوية بإدراج كل التجزئة المتداخلة في القائمة البيضاء بأية مفاتيح مباشرة، ولكن يمكنك استخدام مفاتيح التجزئة المتشعبة (nested hash) لتوضيح ما ترغب في إضافته إلى القائمة البيضاء:

def product_params
  params.require(:product).permit(:name, data: params[:product][:data].try(:keys))
end

الجلسة

يحتوي تطبيقك على جلسة (Session) لكل مستخدم حيث يمكنك تخزين كميات صغيرة من البيانات التي ستستمر بين الطلبات. لا تتوفر الجلسة إلا في وحدة التحكم والملف الشخصي ويمكن استخدام إحدى آليات التخزين المختلفة التالية:

  • ActionDispatch::Session::CookieStore - يخزن كل شيء عند العميل.
  • ActionDispatch::Session::CacheStore - تخزن البيانات في ذاكرة ريلز للتخزين المؤقت.
  • ActionDispatch::Session::ActiveRecordStore - تخزن البيانات في قاعدة بيانات باستخدام Active Record. هذا يتطلب الجوهرة activerecord-session_store.
  • ActionDispatch::Session::MemCacheStore - تخزن البيانات في كتلة memcached (هذا تنفيذ قديم؛ يجب مراعاة استخدام CacheStore بدلًا من ذلك).

تستخدم جميع مخازن جلسات العمل ملف تعريف الارتباط (cookie) لتخزين معرف فريد لكل جلسة (يجب عليك استخدام ملف تعريف الارتباط، ولن تسمح لك ريلز بتمرير معرف الجلسة في عنوان URL لأن هذا أقل أمانًا).

بالنسبة إلى معظم المخازن، يُستخدَم هذا المعرّف ID  للبحث عن بيانات الجلسة على الخادم، على سبيل المثال، في جدول قاعدة البيانات. يوجد استثناء واحد، وهو مخزن الجلسة الافتراضي والموصى به - مخزن ملفات تعريف الارتباط CookieStore - الذي يخزن كافة بيانات جلسة العمل في ملف تعريف الارتباط نفسه (لا يزال المعرف ID متاحًا لك إذا كنت بحاجة إليه). هذا يتميز بكونه خفيف الحجم للغاية ويتطلب إعدادًا صفريًّا (zero setup) في تطبيق جديد من أجل استخدام الجلسة. توقَّع بيانات ملفات تعريف الارتباط لجعلها غير قابلة للتلاعب. كما  تشفر بحيث لا يستطيع أي شخص لديه حق الوصول إليها لقراءة محتوياتها. (لن تقبلها ريلز إذا عُدِّلَت).

يمكن باستعمال الآلية CookieStore تخزين حوالي 4 كيلوبايت من البيانات - أقل بكثير من البيانات الأخرى - ولكن هذا عادةً ما يكون كافيًا. لا تخزن كميات كبيرة من البيانات في الجلسة بغض النظر عن آلية تخزين الجلسة التي يستخدمها تطبيقك. يجب تجنب تخزين الكائنات المعقدة (أي شيء آخر غير كائنات روبي الأساسية، إذ يعدُّ المثال الأكثر شيوعًا هو النماذج) في الجلسة، حيث قد يتعذر على الخادم إعادة تجميعها بين الطلبات، مما يؤدي إلى حدوث خطأ.

إذا لم تخزن جلسات المستخدم الخاصة بك البيانات المهمة أو لا تحتاج إلى أن تكون موجودة لفترات طويلة (على سبيل المثال إذا كنت تستخدم الفلاش [flash] فقط للمراسلة)، فيمكنك استخدام ActionDispatch::Session::CacheStore. سيخزن ذلك الجلسات باستخدام تطبيق ذاكرة التخزين المؤقت الذي قمت بتكوينه للتطبيق الخاص بك. وتتمثل ميزة ذلك في أنه يمكنك استخدام البنية الأساسية المؤقتة الموجودة لديك لتخزين الجلسات دون الحاجة إلى أي إعداد إضافي أو إدارة إضافية. الجانب السلبي، بالطبع، هو أن الجلسات ستكون سريعة الزوال ويمكن أن تختفي في أي وقت.

اقرأ المزيد حول تخزين الجلسة في دليل الأمان.

إذا كنت بحاجة إلى آلية تخزين جلسة عمل مختلفة، فيمكنك تغييرها في المُهيئ (initializer):

# استخدم قاعدة البيانات للجلسات بدلًا من الإعداد الافتراضي الذي يعتمد على 
# ملف تعريف الارتباط، والذي لا ينبغي استخدامه لتخزين معلومات سرية للغاية
# ("rails g active_record: session_migration" أنشئ جدول الجلسة باستخدام)
Rails.application.config.session_store :active_record_store

تقوم ريلز بإعداد مفتاح الجلسة (اسم ملف تعريف الارتباط) عند توقيع بيانات الجلسة. يمكن أيضًا تغيير هذه في المُهيئ:

# تأكد من إعادة تشغيل الخادم الخاص بك عند تعديل هذا الملف.
Rails.application.config.session_store: cookie_store، key: '_your_app_session'

يمكنك أيضًا تمرير المفتاح domain: وتحديد اسم النطاق لملف تعريف الارتباط:

# تأكد من إعادة تشغيل الخادم الخاص بك عند تعديل هذا الملف.
Rails.application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"

إعداد ريلز (من أجل CookieStore) وهو مفتاح سري يستخدم لتوقيع بيانات الجلسة في ‎config/credentials.yml.enc. هذا يمكن تغييره مع bin/rails credentials:edit.

# aws:
#   access_key_id: 123
#   secret_access_key: 345

# في ريلز، بما في ذلك الواحد الذي MessageVerifiers يستخدم كأساس لجميع   
# يحمي ملفات تعريف الارتباط
secret_key_base: 492f...

ملاحظة: سيؤدي تغيير secret_key_base عند استخدام CookieStore إلى إبطال جميع جلسات العمل الموجودة.

الوصول إلى الجلسة

في وحدة التحكم الخاصة بك، يمكنك الوصول إلى الجلسة من خلال نسخة التابع session.

ملاحظة: تُحمَّل الجلسات برفق. إذا لم تدخل إلى الجلسات في شفرة الإجراء الخاص بك، فلن تُحمَّل. ومن ثم لن تحتاج أبدًا إلى تعطيل الجلسات، فقط لن يؤدي الوصول إليها إلى القيام بهذه المهمة.

تخزن قيم الجلسة باستخدام الأزواج مفتاح/قيمة مثل جدول hash:

class ApplicationController < ActionController::Base
 
  private

  # :current_user_id المخزن في الجلسة مع المفتاح ID البحث عن المستخدم بالمعرف  
  # هذه طريقة شائعة للتعامل مع تسجيل دخول المستخدم في تطبيق ريلز؛ تسجيل 
  # الدخول يحدد قيمة الجلسة وتسجيل الخروج يزيلها
  def current_user
    @_current_user ||= session[:current_user_id] &&
      User.find_by(id: session[:current_user_id])
  end
end

لتخزين شيء ما في الجلسة، ما عليك سوى تعيينه إلى المفتاح مثل جدول hash:

class LoginsController < ApplicationController

  # هو تسجيل الدخول والذي يعرف بتسجيل دخول المستخدم "Create" إن
  def create
    if user = User.authenticate(params[:username], params[:password])
     # احفظ مُعرِّف المستخدم في الجلسة حتى يمكن استخدامه في الطلبات اللاحقة
     session[:current_user_id] = user.id
     redirect_to root_url
   end
 end
end

لإزالة شيء ما من الجلسة، عيّن هذا المفتاح ليكون nil:

class LoginsController < ApplicationController
 # هو حذف تسجيل الدخول والذي يعرف بتسجيل خروج المستخدم "Delete" إن
 def destroy
   # قم بإزالة معرِّف المستخدم من الجلسة
   @_current_user = session[:current_user_id] = nil
   redirect_to root_url
 end
end

لإعادة تعيين الجلسة بالكامل، استخدم reset_session.

Flash

إنَّ Flash هو جزء خاص من الجلسة يمسح مع كل طلب. وهذا يعني أن القيم المخزنة هناك لن تكون متاحة إلا في الطلب التالي، وهو أمر مفيد في تمرير رسائل الخطأ وما إلى ذلك.

يتم الوصول إليه بنفس الطريقة مثل الجلسة، مثل hash (إنه نسخة من FlashHash).

لنستخدم فعل تسجيل الخروج كمثال. تستطيع وحدة التحكم إرسال رسالة تعرض على المستخدم عند الطلب التالي:

class LoginsController < ApplicationController
  def destroy
    session[:current_user_id] = nil
    flash[:notice] = "You have successfully logged out."
    redirect_to root_url
  end
end

لاحظ أنه من الممكن أيضًا تعيين رسالة flash كجزء من إعادة التوجيه. يمكنك تعيين ‎:notice، أو ‎:alert أو ‎:flash ذي الغرض العام:

redirect_to root_url, notice: "You have successfully logged out."
redirect_to root_url, alert: "You're stuck here!"
redirect_to root_url, flash: { referral_code: 1234 }

يعيد الإجراء destroy التوجيه إلى root_url للتطبيق، حيث تعرض الرسالة. لاحظ أن الأمر متروك تمامًا للإجراء التالي لتحديد، إذا كان هناك أي شيء، ما إذا كان سينفَّذ الإجراء السابق في flash. من المعتاد عرض أي تنبيهات خطأ أو إشعارات من flash في تخطيط التطبيق:

<html>
  <!-- <head/> -->
  <body>
    <% flash.each do |name, msg| -%>
      <%= content_tag :div, msg, class: name %>
    <% end -%>
 
    <!-- more content -->
  </body>
</html>

بهذه الطريقة، إذا قام أحد الإجراءات بإعداد إشعار أو رسالة تنبيه، فسيعرضه المخطط تلقائيًا. يمكنك تمرير أي شيء يمكن أن تخزنه الجلسة؛ لا تقتصر على الإشعارات والتنبيهات:

<% if flash[:just_signed_up] %>
  <p class="welcome">Welcome to our site!</p>
<% end %>

إذا كنت تريد نقل قيمة flash إلى طلب آخر، فاستخدم طريقة الاحتفاظ:

class MainController < ApplicationController

  # ولكنك ترغب في إعادة توجيه جميع root_url لنفترض أن هذا الإجراء يتوافق مع 
  # flash إذا قام أحد الإجراءات بتعيين .UsersController#index الطلبات هنا إلى
  # وإعادة التوجيه هنا، فستُفقَد القيم عادة عند حدوث عملية إعادة توجيه أخرى
  # لجعلها تستمر لطلب آخر "keep" ولكن يمكنك استخدام
  def index
    # flash سوف تستمر كل قيم .
    flash.keep

    # يمكنك أيضًا استخدام مفتاح للاحتفاظ بنوع معين من القيمة فقط
    # flash.keep(:notice)
    redirect_to users_url
  end
end

Flash.now

بشكل افتراضي، ستؤدي إضافة قيم إلى Flash إلى جعلها متاحة للطلب التالي، ولكن في بعض الأحيان قد ترغب في الوصول إلى هذه القيم في نفس الطلب. على سبيل المثال، إذا فشل إجراء الإنشاء create في حفظ المصدر وقمت بعرض القالب الجديد new مباشرةً، فلن يؤدي ذلك إلى طلب جديد، ولكن قد لا تزال ترغب في عرض الرسالة باستخدام flash. للقيام بذلك، يمكنك استخدام flash.now بنفس طريقة استخدام flash العادي:

class ClientsController < ApplicationController
  def create
    @client = Client.new(params[:client])
    if @client.save
      # ...
    else
      flash.now[:error] = "Could not save client"
      render action: "new"
    end
  end
end

ملفات تعريف الارتباط (Cookies)

يمكن للتطبيق تخزين كميات صغيرة من البيانات على متصفح العميل - تسمى ملفات تعريف الارتباط cookies (أو الكعكات)- والتي ستستمر عبر الطلبات وحتى الجلسات. توفر ريلز وصولًا سهلًا إلى ملفات تعريف الارتباط عبر التابع cookies، والذي - تمامًا مثل الجلسة (session) - يعمل مثل الجدول hash:

class CommentsController < ApplicationController
  def new
    # الملء التلقائي لاسم المعلق إذا كان قد تم تخزينه في ملف تعريف ارتباط
    @comment = Comment.new(author: cookies[:commenter_name])
  end
 
  def create
    @comment = Comment.new(params[:comment])
    if @comment.save
      flash[:notice] = "Thanks for your comment!"
      if params[:remember_name]
        # تذكر اسم صاحب التعليق
        cookies[:commenter_name] = @comment.author
      else
        # حذف ملف تعريف الارتباط لملف تعريف الارتباط الخاص بصاحب التعليق ، إن وجد.
        cookies.delete(:commenter_name)
      end
      redirect_to @comment.article
    else
      render action: "new"
    end
  end
end

لاحظ حينما تقوم بتعيين المفتاح إلى القيمة nil لقيم الجلسة، يجب عليك لحذف قيمة ملف تعريف ارتباط استخدام (cookies.delete(:key.

توفر ريلز أيضًا وعاء ملفات تعريف ارتباط موقَّع ووعاءً لملف تعريف الارتباط مشفر لتخزين البيانات الحساسة. يُلحِق وعاء ملفات تعريف الارتباط توقيعًا مشفرًا على قيم ملفات تعريف الارتباط لحماية تكاملها. يشفِّر وعاء ملف تعريف الارتباط المشفر القيم بالإضافة إلى توقيعها، بحيث لا يمكن للمستخدم النهائي قراءتها. ارجع إلى توثيق الواجهة البرمجية (API) لمزيد من التفاصيل.

تستخدم هذه الأوعية الخاصة لملفات تعريف الارتباط تسلسلًا لإجراء تسلسل للقيم المخصصة في سلاسل وإلغاء تسلسلها إلى كائنات روبي في القراءة.

يمكنك تحديد برنامج التسلسل الذي يجب استخدامه:

Rails.application.config.action_dispatch.cookies_serializer = :json

التسلسل الافتراضي للتطبيقات الجديدة هو ‎:json. للتوافق مع التطبيقات القديمة مع ملفات تعريف الارتباط الموجودة، يُستخدَم ‎:marshal عند عدم تحديد خيار التسلسل.

يمكنك أيضًا تعيين هذا الخيار إلى ‎:hybrid، في هذه الحالة تعمل ريلز على إلغاء تسلسل ملفات تعريف الارتباط الموجودة (Marshal-serialized) بشفافية عند القراءة وإعادة كتابتها بتنسيق JSON. وهذا مفيد لترحيل التطبيقات الحالية إلى مُسلسِل (json (serializer:.

من الممكن أيضًا تمرير مُسلسِل (serializer) مخصص يستجيب للتحميل (load) والتفريغ (dump):

Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer

عند استخدام مسلسل ‎:json أو ‎:hybrid، يجب الحذر من أنه لا يمكن إجراء تسلسل لكافة كائنات روبي بالصيغة JSON. على سبيل المثال، سيُسَلسَل الكائنين Data و Time كسلسلة نصية (strings)، وقيم Hash سوف يكون مفاتيحها سلسلة نصية بصيغة JSON (تدعى stringified).

class CookiesController < ApplicationController
  def set_cookie
    cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
    redirect_to action: 'read_cookie'
  end
 
  def read_cookie
    cookies.encrypted[:expiration_date] # => "2014-03-20"
  end
end

من المستحسن تخزين البيانات البسيطة (السلاسل النصية والأعداد) في ملفات تعريف الارتباط. إذا كان عليك تخزين كائنات معقدة، فستحتاج إلى معالجة التحويل يدويًا عند قراءة القيم في الطلبات اللاحقة.

إذا كنت تستخدم مخزن جلسة ملفات تعريف الارتباط، فسيطبق ذلك على التجزئة session و flash أيضًا.

تصيير بيانات XML و JSON

يجعل ActionController من السهل للغاية تقديم بيانات XML أو JSON. إذا كنت قد أنشأت وحدة تحكم باستخدام scaffolding، فسيبدو الأمر كالتالي:

class UsersController < ApplicationController
  def index
    @users = User.all
    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render xml: @users }
      format.json { render json: @users }
    end
  end
end

قد تلاحظ في الكود السابق أننا استخدمنا render xml: @users وليس render xml: @users.to_xml. إذا لم يكن الكائن عبارة عن سلسلة نصية، فستستدعي ريلز تلقائيًا to_xml من أجلنا.

المرشحات

المرشحات (Filters) هي التوابع التي تُشغِّل إجراءات وحدة التحكم "before"، أو "after" أو "around".

المرشحات موروثة، لذلك إذا قمت بتعيين مرشح على ApplicationController، سيشغل على كل وحدة التحكم في التطبيق الخاص بك.

قد تؤدي مرشحات "before" إلى إيقاف دورة الطلب. المرشح "before" الشائع هو الذي يتطلب تسجيل دخول المستخدم لإجراء ما. يمكنك تحديد طريقة الترشيح بهذه الطريقة:

class ApplicationController < ActionController::Base
  before_action :require_login
 
  private
 
  def require_login
    unless logged_in?
      flash[:error] = "You must be logged in to access this section"
      redirect_to new_login_url # halts request cycle
    end
  end
end

يقوم التابع ببساطة بتخزين رسالة خطأ في الفلاش (flash) ويعيد التوجيه إلى نموذج تسجيل الدخول إذا لم يقم المستخدم بتسجيل الدخول.في حالة عرض أو أعاد المرشح "before" التوجيه، فلن تنفذ الإجراء. إذا كانت هناك مرشحات إضافية مجدولة للتشغيل بعد ذلك المرشح، فستُلغَى أيضًا. في هذا المثال، يضاف المرشح إلى ApplicationController وبالتالي جميع وحدات التحكم في التطبيق ترثه. هذا سيجعل كل شيء في التطبيق يتطلب من المستخدم أن يقوم بتسجيل الدخول من أجل استخدامه. لأسباب واضحة (لن يتمكن المستخدم من تسجيل الدخول في المقام الأول!)، لا ينبغي أن تتطلب كل عناصر التحكم أو الإجراءات ذلك. يمكنك منع تشغيل هذا المرشح قبل تنفيذ إجراءات معينة باستخدام skip_before_action:

class LoginsController < ApplicationController
  skip_before_action :require_login, only: [:new, :create]
end

الآن، سيعمل الإجراءين new و create الذين يخصان LoginsController كما من قبل دون الحاجة إلى تسجيل المستخدم. الخيار ‎:only يُستخدَم فقط لتخطي هذا المرشح لهذه الإجراءات، وهناك أيضًا الخيار ‎:except الذي يعمل بطريقة اخرى. يمكن استخدام هذه الخيارات عند إضافة المرشحات أيضًا، بحيث يمكنك إضافة مرشح لا يعمل إلا للإجراءات المحددة في المقام الأول.

ملاحظة: لن يعمل استدعاء المرشح نفسه عدة مرات بخيارات مختلفة، نظرًا لأن آخر تعريف للمرشح سيحل محل المرشح السابق.

مرشحات After ومرشحات Around

بالإضافة إلى المرشحات "before"، يمكنك أيضًا تشغيل مرشحات بعد تنفيذ إجراء ما، أو قبل وبعد التنفيذ كليهما.

المرشحات "after" تشبه المرشحات "before"، ولكن نظرًا لأنَّ الإجراء قد شُغِّل بالفعل، فسيكون بإمكانهم الوصول إلى بيانات الاستجابة التي على وشك إرسالها إلى العميل. من الواضح أن المرشحات "after" لا يمكنها إيقاف الإجراء من التشغيل. يرجى ملاحظة أن المرشحات "after" تُنفَّذ فقط بعد إجراء ناجح، ولكن ليس عندما يرفع استثناء في دورة الطلب.

المرشحات "around" مسؤولة عن إدارة الإجراءات المرتبطة بها من خلال العائد (yielding)، على غرار الطريقة التي تعمل بها برمجيات Rack الوسيطة.

على سبيل المثال، في أحد مواقع الويب التي تحتوي على تغييرات في سير عمل الموافقة، يمكن للمسؤول معاينتها بسهولة، إذ ما عليك سوى تطبيقها في transaction:

class ChangesController < ApplicationController
  around_action :wrap_in_transaction, only: :show
 
  private
 
  def wrap_in_transaction
    ActiveRecord::Base.transaction do
      begin
        yield
      ensure
        raise ActiveRecord::Rollback
      end
    end
  end
end

لاحظ أنَّ المرشح "around" يلتف أيضًا على العرض. على وجه الخصوص، إذا كان المثال أعلاه مذكورًا في قاعدة البيانات (على سبيل المثال عبر نطاق)، فسيجري ذلك داخل المعاملة وبالتالي تقديم البيانات للمعاينة.

يمكنك عدم اختيار العائد (yield) وبناء الاستجابة بنفسك، وفي هذه الحالة لن ينفذ الإجراء.

طرق أخرى لاستخدام المرشحات

في حين أنَّ الطريقة الأكثر استخدامًا لاستخدام المرشحات هي عن طريق إنشاء توابع خاصة واستخدام action_* لإضافتها، فهناك نوعان من طرق أخرى للقيام بنفس الشيء.

الأول هو استخدام كتلة مباشرة مع التوابع  action_*. تتلقى الكتلة وحدة التحكم كوسيطة. يمكن إعادة كتابة المرشح require_login من الأعلى لاستخدام الكتلة.

class ApplicationController < ActionController::Base
  before_action do |controller|
    unless controller.send(:logged_in?)
      flash[:error] = "You must be logged in to access this section"
      redirect_to new_login_url
    end
  end
end

لاحظ أنَّ المرشح في هذه الحالة يستخدم الإرسال send لأنَّ login_in?‎ تابع خاص ولا يشغل المرشح في نطاق وحدة التحكم. هذه ليست الطريقة الموصى بها لتطبيق هذا المرشح المحدد، ولكن في حالات أكثر بساطة قد يكون ذلك مفيدًا. والطريقة الثانية هي استخدام صنف (في الواقع، أي كائن يستجيب للتوابع الصحيحة يمكن استخدامه) للتعامل مع المرشح. يفيد ذلك في الحالات الأكثر تعقيدًا ولا يمكن تنفيذها بطريقة مقروءة وقابلة لإعادة الاستخدام باستخدام التابعيين الأخريين. على سبيل المثال، يمكنك إعادة كتابة مرشح تسجيل الدخول مرة أخرى لاستخدام صنف:

class ApplicationController < ActionController::Base
  before_action LoginFilter
end
 
class LoginFilter
  def self.before(controller)
    unless controller.send(:logged_in?)
      controller.flash[:error] = "You must be logged in to access this section"
      controller.redirect_to controller.new_login_url
    end
  end
end

مرة أخرى، هذا ليس مثالًا مثاليًا لهذا المرشح، لأنه لا يعمل في نطاق وحدة التحكم ولكنه يحصل على وحدة التحكم التي تمرر كوسيط. يجب أن يطبق صنف المرشح التابع مع الاسم نفسه كمرشح، لذلك، يجب على الفئة أن تقوم بتطبيق التابع before لمرشح before_action، وهكذا يجب على التابع around أن يعيد عبر yield لتنفيذ هذا الإجراء.

طلب حماية التزوير

التزوير في الطلب عبر الموقع (Cross-site request forgery) هو نوع من الهجوم حيث يخدع الموقع مستخدمًا في تقديم طلبات على موقع آخر، وربما إضافة أو تعديل أو حذف البيانات على هذا الموقع دون معرفة المستخدم أو إذنه.

الخطوة الأولى لتجنب هذا هو التأكد من أنه لا يمكن الوصول إلى جميع الإجراءات "المدمرة" (إنشاء وتحديث وتدمير) إلا بطلبات non-GET. إذا كنت تتبع اصطلاحات RESTful التي تفعلها بالفعل. ومع ذلك، لا يزال بإمكان الموقع الخبيث إرسال طلب non-GET إلى موقعك بسهولة تامة، وهنا يأتي طلب حماية التزوير. كما يشير الاسم، فإنه يحمي من الطلبات المزورة.

والطريقة هي إضافة رمز غير قابل للتخمين يعرفه الخادم فقط لكل طلب. بهذه الطريقة، إذا جاء طلب بدون الرمز الصحيح، فسيرفض الوصول.

إذا قمت بإنشاء نموذج مثل هذا:

<%= form_with model: @user, local: true do |form| %>
  <%= form.text_field :username %>
  <%= form.text_field :password %>
<% end %>

سترى كيف يضاف الرمز المميز كحقل مخفي:

<form accept-charset="UTF-8" action="/users/1" method="post">
<input type="hidden"
       value="67250ab105eb5ad10851c00a5621854a23af5489"
       name="authenticity_token"/>
<!-- fields -->
</form>

تضيف ريلز هذا الرمز المميز إلى كل نموذج يُنشَأ باستخدام مساعدي النموذج (form helpers)، لذلك معظم الوقت لا داعي للقلق بشأنه. إذا كنت تكتب نموذجًا يدويًا أو تحتاج إلى إضافة الرمز المميز لسبب آخر، فسيكون متاحًا من خلال التابع form_authenticity_token:

ينشئ form_authenticity_token رمز استيثاق صالح. وهذا مفيد في الأماكن التي لا تضيفها ريلز تلقائيًا، كما هو الحال في استدعاءات Ajax المخصصة.

يحتوي دليل الأمان على مزيد من المعلومات حول هذا الأمر والعديد من المشكلات الأخرى المتعلقة بالأمان والتي يجب أن تكون على دراية بها عند تطوير تطبيق ويب.

كائنات الطلب والاستجابة

يوجد في كل وحدة تحكم تابعين ملحقين يشيران إلى كائنات الطلب و الاستجابة المرتبطة بدورة الطلب قيد التنفيذ حاليًا. يحتوي تابع الطلب request على نسخة من ActionDispatch::Request ويقوم تابع الاستجابة response بإرجاع كائن استجابة يمثل ما الذي سيرسل مرة أخرى إلى العميل.

كائن الطلب

يحتوي الكائن request على الكثير من المعلومات المفيدة حول الطلب القادم من العميل. للحصول على قائمة كاملة بالتوابع المتاحة مع هذا الكائن، راجع توثيق Rails API وتوثيق Rack. من بين الخاصيات التي يمكنك الوصول إليها على هذا الكائن:

خاصية الكائن request الغرض
host اسم المضيف المُستخدَم لهذا الطلب.
domain(n=2)‎ الأجزاء n الأولى من اسم المضيف، تبدأ من اليمين (TLD).
format نوع المحتوى المطلوب من قبل العميل.
method توابع HTTP المستخدمة للطلب.
get?, post?, patch?, put?, delete?, head? تعيد القيمة true إذا كانت طريقة HTTP هي GET / POST / PATCH / PUT / DELETE / HEAD.
headers تعيد التجزئة hash التي تحتوي على العناوين المرتبطة بالطلب.
port رقم المنفذ (عدد صحيح) المستخدم في الطلب.
protocol إرجاع سلسلة تحتوي على البروتوكول المستخدم بالإضافة إلى "‎://‎"، مثل "http://‎".
query_string جزء سلسلة الاستعلام من URL، أي كل شيء بعد الرمز "?".
remote_ip عنوان IP للعميل.
url عنوان URL بأكمله المُستخدَم في الطلب.

معاملات المسار والاستعلام والطلب

تجمع ريلز كافة المعاملات المرسلة مع الطلب في التجزئة params، سواء أُرسلَت كجزء من سلسلة الاستعلام أو نص في post. يحتوي الكائن request على ثلاثة أدوات وصول تمنحك الوصول إلى هذه المعاملات اعتمادًا على مصدرها. تحتوي التجزئة query_parameters على معاملات أُرسلت كجزء من سلسلة الاستعلام بينما تحتوي التجزئة request_parameters على معاملات أُرسلت كجزء من جسم الطلبية post. بينما تحتوي التجزئة path_parameters على معاملات جرى التعرف عليها عليها بواسطة التوجيه باعتبارها جزءًا من المسار المؤدي إلى وحدة التحكم هذه والإجراء المحدد.

كائن الإستجابة

لا يُستخدَم الكائن response عادةً بشكل مباشر، ولكنه يُنشَأ أثناء تنفيذ الإجراء وعرض البيانات التي ترسل إلى المستخدم، ولكن أحيانًا - كما هو الحال في المرشح after - قد يكون من المفيد الوصول إلى الاستجابة مباشرةً. بعض من هذه التوابع ملحق بها أيضا ضابطات (setters)، مما يتيح لك ضبط وتغيير قيمها. للحصول على قائمة كاملة بالتوابع المتاحة، راجع توثيق Rails API وتوثيق Rack.

خاصية الكائن response الغرض
body هذه هي سلسلة البيانات التي يتم إرسالها مرة أخرى إلى العميل. هذا هو في الغالب HTML.
status رمز حالة HTTP للاستجابة، مثل 200 لطلب ناجح أو 404 لطلب لم يتم العثور عليه.
location عنوان URL الذي تتم إعادة توجيه العميل إليه، إن وجد.
content_type نوع محتوى الاستجابة.
charset ترميز المحارف المستخدمة للاستجابة. الترميز الافتراضي هو "utf-8".
headers العناوين التي تستخدم للاستجابة.

ضبط عناوين مخصصة

إذا كنت ترغب في تعيين عناويين مخصصة للاستجابة، فإن response.headers هو المكان المناسب للقيام بذلك. تعتبر الخاصية headers عبارة عن جدول hash تعين أسماء العناوين إلى قيمها، وسوف تعين ريلز بعضها تلقائيًا. إذا كنت ترغب في إضافة عنوان أو تغييره، ما عليك سوى تعيينه عبر response.headers على هذا النحو:

response.headers["Content-Type"] = "application/pdf"

ملاحظة: في الحالة المذكورة أعلاه، قد يكون من الأفضل استخدام الضابط content_type مباشرةً.

HTTP استيثاق

تأتي ريلز مع آليتي استيثاق HTTP مدمجتين:  

  • استيثاق أساسي (Basic Authentication).
  • استيثاق عبر قيم hash مختصرة (Digest Authentication).

استيثاق HTTP الأساسي

استيثاق HTTP الأساسي هو نظام استيثاق تدعمه غالبية المتصفحات وعملاء HTTP الآخرين. على سبيل المثال، ضع في الاعتبار قسم الإدارة الذي سيكون متاحًا فقط عن طريق إدخال اسم مستخدم وكلمة مرور في نافذة حوار HTTP الأساسية للمتصفح. يعد استخدام الاستيثاق المدمج سهلًا تمامًا ولا يتطلب منك سوى استخدام تابع واحد مثل http_basic_authenticate_with.

class AdminsController < ApplicationController
  http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
end

مع هذا، يمكنك إنشاء وحدات تحكم ضمن مجال أسماء (namespaced controllers) ترث من AdminsController. وبالتالي، سيُشغَّل المرشح لجميع الإجراءات في وحدات التحكم هذه، مع حمايتها باستخدام استيثاق HTTP الأساسي.

استيثاق طلبية HTTP عبر قيم hash مختصرة

يتفوق استيثاق طلبية HTTP عبر قيم hash مختصرة (HTTP digest authentication) على الاستيثاق الأساسي لأنه لا يتطلب من العميل إرسال كلمة مرور غير مشفرة عبر الشبكة (على الرغم من أن استيثاق HTTP الأساسي آمن عبر HTTPS). يعد استخدام استيثاق HTTP Digest مع ريلز أمرًا سهلاً للغاية ولا يتطلب سوى استخدام تابع واحد وهو Authenticate_or_request_with_http_digest.

class AdminsController < ApplicationController
  USERS = { "lifo" => "world" }
 
  before_action :authenticate
 
  private
 
    def authenticate
      authenticate_or_request_with_http_digest do |username|
        USERS[username]
      end
    end
end

كما هو موضح في المثال أعلاه، تأخذ كتلة authenticate_or_request_with_http_digest وسيطًا واحدًا فقط وهو اسم المستخدم. وتعيد الكتلة كلمة المرور. سيؤدي إعادة القيمة false أو لا شيء من authenticate_or_request_with_http_digest إلى حدوث فشل في الاستيثاق.

التدفق وتحميل الملفات

في بعض الأحيان قد ترغب في إرسال ملف إلى المستخدم بدلًا من عرض صفحة HTML. جميع وحدات التحكم في ريلز لها التابع send_data و send_file، وكلاهما سوف ترسل البيانات إلى العميل عبر مجرى (stream). إن send_file هو تابع ملائم يسمح لك بتوفير اسم الملف على لتنزيله إلى حاسوب العميل وسوف تتدفق محتويات هذا الملف لتنزيله. لبدء تدفق البيانات إلى العميل، استخدم send_data:

require "prawn"
class ClientsController < ApplicationController
  # مع معلومات عن العميل وإعادته PDF توليد ملف  
  # PDF سيحصل المستخدم على الملف لتنزيله كملف
  def download_pdf
    client = Client.find(params[:id])
    send_data generate_pdf(client),
              filename: "#{client.name}.pdf",
              type: "application/pdf"
  end
 
  private
 
    def generate_pdf(client)
      Prawn::Document.new do
        text client.name, align: :center
        text "Address: #{client.address}"
        text "Email: #{client.email}"
      end.render
    end
end

سيقوم الإجراء download_pdf في المثال أعلاه باستدعاء تابع خاص يقوم بالفعل بإنشاء مستند PDF وإعادتها كسلسلة. بعد ذلك ستبدأ هذه السلسلة إلى العميل بالتدفق لتنزيل الملف وسيُقترَح اسم الملف للمستخدم. في بعض الأحيان، عند تدفق الملفات إلى المستخدم، قد لا ترغب في تنزيل الملف. التقط صورًا، على سبيل المثال، لتضمينها في صفحات HTML. لإخبار المتصفح أنه ليس من المفترض تنزيل أحد الملفات، يمكنك تعيين الخيار ‎:disposition إلى القيمة "inline". القيمة المقابلة والافتراضية لهذا الخيار هي "attachment".

إرسال الملفات

إذا كنت ترغب في إرسال ملف موجود بالفعل على القرص، فاستخدم التابع send_file.

class ClientsController < ApplicationController
  # إرسال الملف الذي جرى توليده مسبقًا لتخزينه على جهاز العميل
  def download_pdf
    client = Client.find(params[:id])
    send_file("#{Rails.root}/files/clients/#{client.id}.pdf",
              filename: "#{client.name}.pdf",
              type: "application/pdf")
  end
end

سيؤدي ذلك إلى قراءة الملف وتسجيله بسرعة 4 كيلوبايت في ذلك الوقت، مع تجنب تحميل الملف بأكمله إلى الذاكرة مرة واحدة. يمكنك إيقاف تشغيل التدفق باستخدام الخيار ‎:stream أو ضبط حجم الكتلة باستخدام الخيار ‎:buffer_size.

إذا لم يحدَّد الخيار ‎:type، فسيُخمَّن من امتداد الملف المحدد في ‎:filename. إذا كان نوع المحتوى غير مسجل للإمتداد، فسيُستخدَم application/octet-stream.

تحذير: كن حذرًا عند استخدام البيانات القادمة من العميل (المعاملات، ملفات تعريف الارتباط، وما إلى ذلك) لتحديد موقع الملف على القرص، لأن ذلك يمثل خطرًا أمنيًا قد يسمح لشخص ما بالوصول إلى الملفات لا يفترض أن يصل إليها.

فائدة: لا يوصى بأن تقوم ببث ملفات ثابتة عبر ريلز إذا كان يمكنك بدلاً من ذلك الاحتفاظ بها في مجلد عام على خادم الويب الخاص بك. إنه أكثر فاعلية للسماح للمستخدم بتنزيل الملف مباشرة باستخدام خادم Apache أو خادم ويب آخر، مع الحفاظ على الطلب من دون داعٍ من خلال مكدس ريلز بأكمله.

تنزيلات RESTful

لمَّا كان send_data يعمل على ما يرام، فإنَّه إذا كنت تقوم بإنشاء تطبيق RESTful مع وجود إجراءات منفصلة لتنزيل الملفات، فعادةً ما تكون غير ضرورية. في مصطلح REST، يمكن اعتبار ملف بصيغة PDF من المثال أعلاه مجرد تمثيل آخر لمورد العميل. توفر ريلز طريقةً سهلةً وميسرةً للغاية للقيام "بتنزيلات RESTful". في ما يلي كيفية إعادة كتابة المثال بحيث يكون تنزيل ملف PDF جزءًا من الإجراء show، دون أي تدفق (streaming):

class ClientsController < ApplicationController
  # HTML يمكن أن يرسل المستخدم طلبًا لاستقبال هذا المورد بهيئة صفحة
  # PDF أو ملف
  def show
    @client = Client.find(params[:id])
 
    respond_to do |format|
      format.html
      format.pdf { render pdf: generate_pdf(@client) }
    end
  end
end

لكي يعمل هذا المثال، يجب عليك إضافة نوع PDF MIME إلى ريلز. يمكن القيام بذلك عن طريق إضافة السطر التالي إلى ملف config/initializers/mime_types.rb:

Mime::Type.register "application/pdf", :pdf

لا يعاد تحميل ملفات التهيئة على كل طلب، لذا يجب عليك إعادة تشغيل الخادم حتى تُفعَّل التغييرات التي أُجريت. الآن يمكن للمستخدم طلب الحصول على نسخة PDF من عميل فقط عن طريق إضافة "‎.pdf" إلى عنوان URL:

GET /clients/1.pdf

تدفق مباشر للبيانات الإعتباطية

تسمح لك ريلز بتدفق أكثر من مجرد ملفات. في الواقع، يمكنك بث أي شيء تريده في الكائن response. تسمح لك الوحدة ActionController::Live بإنشاء اتصال دائم مع المتصفح. باستخدام هذه الوحدة، ستتمكن من إرسال بيانات اعتباطية إلى المتصفح في أوقات محددة.

دمج التدفق المباشر

تضمين ActionController::Live داخل صنف وحدة التحكم الخاصة بك سيوفر كافة الإجراءات داخل وحدة التحكم للقدرة على تدفق البيانات. يمكنك الخلط في الوحدة بالشكل التالي:

class MyController < ActionController::Base
  include ActionController::Live
 
  def stream
    response.headers['Content-Type'] = 'text/event-stream'
    100.times {
      response.stream.write "hello world\n"
      sleep 1
    }
  ensure
    response.stream.close
  end
end

ستحافظ التعليمة البرمجية المذكورة أعلاه على اتصال مستمر مع المتصفح و إرسال 100 رسالة من "hello world \ n"، كل ثانية واحدة منفصلة.

هناك أمران يجب ملاحظتهما في المثال أعلاه. نحن بحاجة للتأكد من إغلاق تدفق الاستجابة. نسيان إغلاق التدفق سيترك المقبس مفتوحًا للأبد. يتعين علينا أيضًا تعيين نوع المحتوى على text/event-stream قبل أن نكتب إلى تدفق الاستجابة. هذا لأن العناوين لا يمكن كتابتها بعد أن نُفِّذَت الاستجابة (عندما تعيد ?response.committed قيمة صحيحة)، والتي تحدث عند كتابة أو تنفيذ تدفق الاستجابة عبر write أو commit.

مثال عملي

لنفترض أنك كنت تصنع آلة كاريوكي (Karaoke machine) ويريد المستخدم الحصول على كلمات أغنية معينة. كل أغنية لها عدد معين من الخطوط وكل سطر يستغرق وقتًا num_beats لإنهاء الغناء.

إذا أردنا إرجاع كلمات أغنية الكاريوكي (إرسال الخط فقط عندما ينتهي المغني من الخط السابق)، فيمكننا استخدام ActionController::Live على النحو التالي:

class LyricsController < ActionController::Base
  include ActionController::Live
 
  def show
    response.headers['Content-Type'] = 'text/event-stream'
    song = Song.find(params[:id])
 
    song.each do |line|
      response.stream.write line.lyrics
      sleep line.num_beats
    end
  ensure
    response.stream.close
  end
end

ترسل الشيفرة السابقة السطر التالي فقط بعد اكمال المغني للخط السابق.

اعتبارات التدفق

يعد بث البيانات الاعتباطية أداة قوية للغاية. كما هو موضح في الأمثلة السابقة، يمكنك اختيار متى وماذا يجب إرساله عبر تدفق الاستجابة. ومع ذلك، يجب عليك أيضًا ملاحظة الأشياء التالية:

  • ينشئ كل تدفق استجابة مؤشر ترابط جديد وينسخ عبر متغيرات مؤشر الترابط المحلي من مؤشر الترابط الأصلي. وجود العديد من المتغيرات المحلية لمؤشر الترابط يمكن أن يؤثر سلبًا على الأداء. وبالمثل، فإن عددًا كبيرًا من سلاسل الرسائل يمكن أن يعوق الأداء أيضًا.
  • سيؤدي الفشل في إغلاق مسار الاستجابة إلى إغلاق المقبس المطابق للأبد. تأكد من إغلاق الاتصال عندما تستخدم تدفق استجابة.
  • خوادم WEBrick تخزِّن جميع الردود، وهكذا لن تعمل ActionController::Live. يجب عليك استخدام خادم ويب لا يقوم تلقائيًا بتخزين الاستجابات.

ترشيح السجل

تحتفظ ريلز بملف سجل (log) لكل بيئة في مجلد السجل. هذا مفيد للغاية عند تصحيح ما يحدث بالفعل في التطبيق الخاص بك، ولكن في تطبيق مباشر قد لا ترغب في تخزين كل جزء من المعلومات في ملف السجل.

ترشيح المعاملات

يمكنك ترشيح معاملات الطلب الحساسة من ملفات السجل الخاصة بك عن طريق إلحاقها بـ config.filter_parameters في ضبط التطبيق. ستوضع علامة على هذه المعاملات بالشكل [FILTERED] في السجل.

config.filter_parameters << :password

ملاحظة: ستُرشَح المعاملات المقدمة بواسطة التعبير النمطي المتطابق جزئيًّا. تضيف ريلز الخيار ‎:password افتراضيًّا في المهيئ (initializer) المناسب (initializers/filter_parameter_logging.rb) وتهتم بمعاملات التطبيق النموذجية password و password_confirmation.

ترشيح إعادة التوجيه

من المستحسن أحيانًا ترشيح بعض ملفات المواقع الحساسة التي يعيد تطبيقك التوجيه إليها من ملفات السجل. يمكنك القيام بذلك عن طريق استخدام خيار الضبط config.filter_redirect:

config.filter_redirect << 's3.amazonaws.com'

يمكنك تعيينه إلى سلسلة نصية، أو تعبير نمطي، أو مصفوفة على حد سواء.

config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]

عناوين URL المتطابقة يشار إليها بالعلامة "[FILTERED]".

معالجة الاستثناءات

على الأرجح سيحتوي التطبيق الخاص بك على أخطاء بطريقة أو بأخرى وسيُطلق استثناءات تحتاج إلى معالجة. على سبيل المثال، إذا اتبع المستخدم رابطًا لمورد لم يعد موجودًا في قاعدة البيانات، فسيلقي Active Record الاستثناء ActiveRecord::RecordNotFound.

يعرض معالج الاستثناء الافتراضي في ريلز الرسالة "500 Server" لكافة الاستثناءات. إذا أنشئ الطلب محليًا، فسيعرض تتبع جيد وبعض المعلومات المضافة حتى تتمكن من معرفة الخطأ الذي حدث والتعامل معه. أمَّا إذا كان الطلب عن بُعد، فستعرض ريلز الرسالة "500 Server Error" البسيطة للمستخدم، أو الرسالة "404 Not Found" إذا كان هناك خطأ في التوجيه أو تعذر العثور على سجل ما. في بعض الأحيان قد ترغب في تخصيص كيفية اكتشاف هذه الأخطاء وكيفية عرضها للمستخدم. توجد عدة مستويات من معالجة الاستثناءات التي توفرها ريلز.

قوالب الخطأ 500 والخطأ 404 الافتراضية

بشكل افتراضي، سيعرض تطبيق الإنتاج رسالة الخطأ 404 أو 500، في بيئة التطوير تطلق كافة الاستثناءات غير المعالجة. تضمن هذه الرسائل في ملفات HTML الثابتة في المجلد العام ، في الملف ‎404.html و ‎500.html على التوالي. يمكنك تخصيص هذه الملفات لإضافة بعض المعلومات والتنسيقات الإضافية، ولكن تذكر أنها ملفات HTML ثابت؛ بمعنى أنه لا يمكنك استخدام ERB أو SCSS أو CoffeeScript أو مخططات معها.

الموجه Rescue_from

إذا كنت تريد القيام بشيء أكثر تفصيلًا عند اكتشاف الأخطاء، فيمكنك استخدام rescue_from، الذي يعالج استثناءات من نوع معين (أو أنواع متعددة) في وحدة تحكم كاملة والفئات الفرعية الخاصة بها.

عند حدوث استثناء اكتُشِف بواسطة الموجه rescue_from، يمرر كائن الاستثناء إلى المعالج. يمكن أن يكون المعالج تابع أو كائن Proc مرر الى الخيار ‎:with. يمكنك أيضًا استخدام الحظر مباشرةً بدلًا من الكائن Proc الصريح.

إليك كيف يمكنك استخدام rescue_from لاعتراض جميع الأخطاء ActiveRecord::RecordNotFound وفعل شيء ما معهم.

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
 
  private
 
    def record_not_found
      render plain: "404 Not Found", status: 404
    end
end

بالطبع، هذا المثال هو أي شيء سوى أن يكون أكثر تفصيلًا إذ لا يحسن التعامل مع الاستثناءات الافتراضية على الإطلاق، ولكن بمجرد أن تتمكن من التقاط كل هذه الاستثناءات، فأنت حر في أن تفعل ما تريده معهم. على سبيل المثال، يمكنك إنشاء فئات استثناء مخصصة ستطلق عندما لا يكون لدى المستخدم حق الوصول إلى قسم معين من التطبيق الخاص بك:

class ApplicationController < ActionController::Base
  rescue_from User::NotAuthorized, with: :user_not_authorized
 
  private
 
    def user_not_authorized
      flash[:error] = "You don't have access to this section."
      redirect_back(fallback_location: root_path)
    end
end
 
class ClientsController < ApplicationController
  # (clients) تحقق أنَّ المستخدم لديه الصلاحيات المناسبة للوصول إلى العملاء
  before_action :check_authorization
 
  # لا تهتم بأي شيء متعلق بالصلاحيات والرخص (actions) انتبه إلى أنَّ الإجراءات
  def edit
    @client = Client.find(params[:id])
  end
 
  private
 
    # إن كان المستخدم غير موثوق ولا يملك الصلاحيات المناسبة، فسيرمى استثناء
    def check_authorization
      raise User::NotAuthorized unless current_user.admin?
    end
end

تحذير: قد يتسبب استخدام rescue_from مع Exception أو StandardError في حدوث آثار جانبية خطيرة حيث تمنع ريلز من التعامل مع الاستثناءات بشكل صحيح. على هذا النحو، لا ينصح باستعمال rescue_from إلا إذا كان هناك سبب قوي لذلك.

ملاحظة: عند التشغيل في بيئة الإنتاج، تعرض كافة الأخطاء ActiveRecord::RecordNotFound صفحة الخطأ 404. ما لم تكن بحاجة إلى سلوك مخصص، فأنت لا تحتاج إلى التعامل مع هذا الخطأ.

ملاحظة: بعض الاستثناءات المحددة هي فقط قابلة للانقاذ (rescuable) من الصنف ApplicationController، كما أنها تُطلَق قبل أن تهيئ وحدة التحكم وأن ينفذ الإجراء.

فرض بروتوكول HTTPS

في وقت ما قد ترغب في فرض وحدة تحكم معينة على الوصول إليها فقط عبر بروتوكول HTTPS لأسباب أمنية. يمكنك استخدام طريقة force_ssl في وحدة التحكم لفرض ما يلي:

class DinnerController
  force_ssl
end

تمامًا مثل المرشح، يمكنك أيضًا تمرير الخيار ‎:only و ‎:except لفرض الاتصال الآمن فقط على إجراءات محددة:

class DinnerController
  force_ssl only: :cheeseburger
  # أو
  force_ssl except: :cheeseburger
end

الرجاء ملاحظة أنه إذا وجدت نفسك تضيف force_ssl إلى العديد من وحدات التحكم، فقد تحتاج إلى فرض استخدام التطبيق بأكمله على HTTPS بدلًا من ذلك. في هذه الحالة، يمكنك ضبط config.force_ssl في ملف البيئة الخاص بك.

المصادر