مقال تقني

FieldMDP في Delphi: كشف التعديلات على حقول النماذج المقفلة

يجيب PDFlibPas عن سؤال FieldMDP عبر TPDFlib.GetSignatureModificationVerdict. إذا أعطيت الطريقة ملف PDF موقّعاً وكلمة مروره واسم حقل توقيع، فستمر على كل تحديث تدريجي (incremental update) أُلحق بعد ذلك التوقيع، وتحل الاسم المؤهل بالكامل (fully qualified name) لكل حقل نموذج لمسته تلك التحديثات، ثم تعيد 3 إذا تغيّر حقل مقفل بواسطة FieldMDP و2 إذا اقتصر التغيير على حقول غير مقفلة. بعد ذلك تسمي GetSignatureModificationDetail المراجعة والحقل المعني

سير العمل الذي يحتاج هذا هو نموذج الموافقات المتعددة. يملأ الطالب Order.Customer و Order.Total في طلب شراء ثم يوقّع على Approval.Requester فيقفل الحقلين. يضيف المدير مركز تكلفة ويوقّع على Approval.Manager. بعد أسبوعين يفتح أحدهم الملف في محرر مكتبي ويرفع قيمة Order.Total في تحديث تدريجي ثالث. كل توقيع يظل صالحاً عند التحقق، لأن كل واحد منها يغطي نطاق البايتات الخاص به فقط. أما الأسئلة التي يطرحها المدقق فعلاً فهي: أي حقل تغيّر بعد أي توقيع، وهل كان هذا التغيير مسموحاً

لماذا يعجز DocMDP وحده عن كشف تعديل حقل مقفل؟

يعجز DocMDP عن كشفه لأن DocMDP يصنف أنواع التغييرات لا الحقول الفردية. مستويات أذونات الاعتماد (certification) في ISO 32000-1 §12.8.2.2 تحدد ما إذا كان يحق للتحديثات اللاحقة تعبئة النماذج والتوقيع (P 2) أو إضافة التعليقات أيضاً (P 3)، وهذا الإذن ينطبق على كل حقل في المستند بالتساوي. تعديل Order.Total هو مجرد تعبئة نموذج، فاعتماد P 2 يظل مستوفى رغم التحرير نفسه الذي أراد الطالب منعه. يغطي تصنيف التعديلات على مستوى المستند في PDFlibPas تلك الطبقة، وهذه المقالة تنزل درجة أدق منها

أما FieldMDP فهي طبقة دقة الحقل الواحد. يمكن لحقل توقيع أن يحمل قاموس /Lock‏ (ISO 32000-1 §12.7.4.5)، ويمكن للتوقيع الذي يحمله أن يتضمن تحويل FieldMDP (transform) في مرجع التوقيع (ISO 32000-1 §12.8.2.4). وكلاهما يستخدم المفردات نفسها: /Action /All يقفل كل الحقول، و /Action /Include يقفل الأسماء المذكورة في /Fields، و /Action /Exclude يقفل كل شيء عدا الأسماء في /Fields. قبل الإصدار v3.586.0 كانت GetSignatureModificationVerdict تعرف أن مراجعة لاحقة قد غيّرت حقلاً من حقول النموذج لكنها لا تعرف أيها، فأي مستند يحمل قفلاً ميدانياً تجاوزت تحديثاته اللاحقة مواد التحقق كان يعود بالقيمة 4 أي غير محدد. تسجل خريطة المراجعات (revision map) الآن اسم حقل محلولاً لكل كائن متعلق بالحقول تصنفه، وهذا بالضبط ما يحول القيمة 4 إلى 2 أو 3 حقيقية

كتابة القفل الميداني عند توقيع كل معتمد

تضبط TPDFlib.SetSignProcessFieldLock القفل الخاص بالتوقيع التالي الذي ينتجه عمل التوقيع. الإجراء 1 هو All، و2 هو Include، و3 هو Exclude، و0 يمسح القفل؛ وتأخذ Include و Exclude قائمة أسماء حقول مؤهلة بالكامل تفصلها فواصل منقوطة. ينتهي الحقل الموقّع إلى قاموس /Lock وتحويل FieldMDP مطابق، وهذا هو الزوج الذي تتوقعه المدققات الصارمة (validators)

رموز إجراءات SetSignProcessFieldLock في PDFlibPas للتوقيع بنمط FieldMDP داخل Delphi: الإجراء 1‏ All يقفل كل حقول النموذج بما فيها حقول التوقيع اللاحقة، والإجراء 2‏ Include يقفل حقول البيانات المسماة، والإجراء 3‏ Exclude يقفل كل شيء عدا القائمة، والإجراء 0 يمسح القفل
اختر Include لحقول البيانات التي يملكها كل معتمد أو Exclude مع سرد حقول التوقيع اللاحقة؛ والاستدعاء المرفوض يعيد 0 ويترك القفل السابق قائماً، لذا تحقق من القيمة المعادة دائماً
var
  Pdf: TPDFlib;
  SignId: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    SignId := Pdf.NewSignProcessFromFile('purchase-request.pdf', '');
    if SignId = 0 then
      raise Exception.Create('cannot open the form');
    try
      Pdf.SetSignProcessField(SignId, 'Approval.Requester');
      Pdf.SetSignProcessPFXFromFile(SignId, 'requester.pfx', PfxPassword);
      // 2 = Include: هذا التوقيع يجمّد هذه الحقول فقط
      if Pdf.SetSignProcessFieldLock(SignId, 2,
        'Order.Customer;Order.Total') <> 1 then
        raise Exception.Create('field lock rejected');
      Pdf.EndSignProcessToFile(SignId, 'purchase-request-signed.pdf');
      if Pdf.GetSignProcessResult(SignId) <> 1 then
        raise Exception.CreateFmt('signing failed, code %d',
          [Pdf.GetSignProcessResult(SignId)]);
    finally
      Pdf.ReleaseSignProcess(SignId);
    end;
  finally
    Pdf.Free;
  end;
end;

تعيد SetSignProcessFieldLock القيمة 0 عند استدعاء Include أو Exclude بلا أسماء، وكذلك عند قائمة تحتوي عنصراً فارغاً مثل 'Order.Customer;;Order.Total'؛ والاستدعاء المرفوض يترك القفل المضبوط سابقاً في مكانه، فتحقق من القيمة المعادة ولا تفترض أن الاستدعاء الأخير نجح. أما بقية إعداد التوقيع، اختيار خوارزمية التجزئة (digest) وحجز /Contents ورموز النتائج، فمغطى في دليل التوقيع والتحقق PAdES لـ PDFlibPas. ونقطة تصميم واحدة خاصة بسلاسل الموافقات: قفل /All على توقيع الطالب يجمد Approval.Manager أيضاً، لأن حقل التوقيع نفسه حقل نموذج. استخدم Include لحقول البيانات التي يملكها كل معتمد، أو Exclude مع سرد حقول التوقيع اللاحقة

تشغيل الحكم عبر سلسلة موافقات كاملة

يحكم GetSignatureModificationVerdict على توقيع واحد في كل مرة، لذا تُفحص سلسلة الموافقات باستدعائه مرة واحدة لكل حقل توقيع. القيمة المعادة تنتمي إلى مجموعة صغيرة مغلقة، ولكل قيمة معنى مختلف لدى المراجع:

  • 0: تعذر قراءة الملف أو حقل التوقيع المسمى
  • 1: لم يُلحق شيء بعد التوقيع
  • 2: لم تُلحق سوى تغييرات تجيزها الأذونات الفعلية، ولم يُمس أي حقل مقفل
  • 3: أجرت مراجعة لاحقة تغييراً غير مسموح، بما في ذلك تعديل حقل يقفله FieldMDP
  • 4: تعذر تحديد الجواب، ولا يجوز أبداً قراءته كمصادقة
  • 5: التوقيع لا ينتهي عند حد مراجعة، فثمة بايتات داخل النطاق الذي يدّعي تغطيته
const
  Approvals: array[0..2] of WideString =
    ('Approval.Requester', 'Approval.Manager', 'Approval.Finance');
var
  Pdf: TPDFlib;
  I, Verdict: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    for I := Low(Approvals) to High(Approvals) do
    begin
      Verdict := Pdf.GetSignatureModificationVerdict(
        'purchase-request-final.pdf', FormPassword, Approvals[I]);
      // كلتا الدالتين تصفان الاستدعاء الذي جرى للتو، فاقرأهما قبل الاستدعاء التالي
      Log.Add(Format('%s: verdict %d, MDP level %d, %s',
        [Approvals[I], Verdict, Pdf.GetSignatureEffectiveMDPLevel,
         Pdf.GetSignatureModificationDetail]));
      case Verdict of
        1, 2: ;                                 // مقبول
        3: RejectForm(Approvals[I]);            // التفصيل يسمي الحقل والمراجعة
      else
        QueueForManualReview(Approvals[I]);     // 0, 4, 5
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

خذ مثال طلب الشراء المعبث فيه، حيث المراجعة 0 هي النموذج الفارغ، والمراجعتان 1 و2 هما التوقيعان، والمراجعة 3 هي التحرير. ولأن تعريفات الأقفال تُجمع من كل حقل موقّع، فإن الحكم على Approval.Manager يفرض قفل الطالب أصلاً، وتحديثه اللاحق الوحيد هو التحرير نفسه. عندما يعيد ذلك التحديث كتابة قاموس الحقل وحده، يكون الحكم 3 ويقرأ التفصيل Revision 3 changed locked form field "Order.Total", which FieldMDP does not allow. هذا السطر الواحد يجيب عن سؤالي التدقيق معاً. أما الحكم على Approval.Requester فيعبر أولاً عن مراجعة توقيع المدير، وللأسباب المذكورة في قسم القيود أدناه سيبلغ غالباً 4، لذا فالتوقيع الأحدث هو عادة من يعطي أدق جواب. يبلّغ GetSignatureEffectiveMDPLevel بأدنى قيمة DocMDP P وُجدت بين التواقيع، أو 3 عندما لم يعتمد شيء المستند، وكلتا الدالتين تبقيان صالحتين حتى استدعاء الحكم التالي فقط

خط زمني للتحديثات التدريجية لنموذج Delphi PDF معبث فيه كما تقرأه PDFlibPas GetSignatureModificationVerdict: المراجعة 1 تحمل قفل Include على Order.Customer و Order.Total، والمراجعة 2 توقّع Approval.Manager، وإعادة كتابة المراجعة 3 للحقل المقفل Order.Total تنتج الحكم 3 لتوقيع المدير و4 لتوقيع الطالب
تُجمع الأقفال من كل حقل موقّع، فيفرض الحكم على Approval.Manager قفل الطالب أصلاً وتسمي سلسلة التفصيل الحقل والمراجعة المخالفين، بينما كثيراً ما يقع أقدم توقيع على القيمة 4

كيف يعرف PDFlibPas أي حقل غيّرته المراجعة؟

يعيد PDFlibPas بناء اسم الحقل من مخطط الكائنات (object graph) لكل مراجعة، لأن الكائن المتغير نادراً ما يحمل اسمه الكامل. قاموس الحقل الذي يحمل /FT أو /V، أو التعليق من نوع widget، يحفظ اسمه الجزئي فقط في /T. والاسم المؤهل بالكامل الموصوف في ISO 32000-1 §12.7.3.2 هو سلسلة الأسماء الجزئية من الجذر متصلة بنقاط، لذا تمشي خريطة المراجعات صعوداً عبر روابط /Parent وتسبق كل /T تجده. ويُقرأ كل أب من أحدث تعريف له عند المراجعة المصنفة أو قبلها، وهذا يمنع إعادة تسمية لاحقة لعقدة أب من إعادة كتابة تاريخ التحريرات الأسبق

حل الاسم المؤهل بالكامل في FieldMDP داخل PDFlibPas لـ Delphi: تمشي خريطة المراجعات صعوداً عبر روابط /Parent من الـ widget المتغير، وتسبق كل اسم جزئي /T متصلاً بنقاط لبناء Order.Total، ثم تطابقه مع مدخلات /Fields بمطابقة تامة أو ببادئة منقوطة فيغطي القفل على Order كل أحفاده
الكائن المتغير نادراً ما يحمل اسمه الكامل: تُقرأ العقد الأب من أحدث تعريف لها عند المراجعة المصنفة أو قبلها، وتُقارن الأسماء بحساسية حالة الأحرف، ويتوقف السير بعد ثمانية مستويات

مطابقة الاسم المحلول مع الأقفال بسيطة عن قصد. يطابق الاسم مدخلاً في /Fields عندما يساويه تماماً أو يبدأ به يليه نقطة، فقفل Order يقفل Order.Total وكل سليل آخر. يقفل All كل اسم، ويقفل Include الأسماء المطابقة، ويقفل Exclude كل اسم غير مطابق. وتأتي تعريفات الأقفال من كل حقل توقيع موقّع في الملف؛ وحيثما جمع حقلٌ بين قاموس /Lock وتحويل FieldMDP، تُستخدم قيم /Lock. ولمعرفة ما قفله كل توقيع بدقة، تكشف الصنف الأدنى TPDFlibSignDoc من وحدة PDFlibDigitalSign تقرير الأذونات الذي يقرأه الحكم:

uses
  PDFlibDigitalSign;

var
  SignDoc: TPDFlibSignDoc;
  Report: TPDFlibSignaturePermissionReport;
  Names: TStringList;
  I, J: Integer;
  Line: string;
begin
  SignDoc := TPDFlibSignDoc.Create;
  Names := TStringList.Create;
  try
    if not SignDoc.Open('purchase-request-final.pdf', FormPassword, False) then
      Exit;
    try
      SignDoc.GetSignatureFieldNames(Names);
      for I := 0 to Names.Count - 1 do
      begin
        SignDoc.GetSignaturePermissionInfoByName(Names[I], Report);
        if not (Report.HasFieldLock or Report.HasFieldMDP) then
          Continue;
        Line := Names[I] + ': ' + string(Report.FieldLockAction);
        for J := 0 to High(Report.FieldNames) do
          Line := Line + ' ' + string(Report.FieldNames[J]);
        if Report.HasFieldLock and Report.HasFieldMDP and
          not Report.FieldMDPMatches then
          Line := Line + ' (lock and FieldMDP transform disagree)';
        Log.Add(Line);
      end;
    finally
      SignDoc.Close;
    end;
  finally
    Names.Free;
    SignDoc.Free;
  end;
end;

النماذج المشفرة: مرر كلمة المرور وإلا بقيت الأسماء غير مقروءة

لا يمكن حل أي اسم حقل في نموذج مشفر قبل فك تشفيره، لذا تحتاج GetSignatureModificationVerdict وسيط Password لغرض أعمق من مجرد فتح الملف. السلاسل مثل /T مشفرة لكل كائن على حدة، والحقول المكتوبة داخل تدفقات كائنات مضغوطة (compressed object streams) لا تُقرأ إطلاقاً حتى يُفك تشفير التدفق الحاوي ويُفك ضغطه. ومنذ v3.586.0 تقبل خريطة المراجعات الداخلية كلمة المرور، وترفق سياق فك تشفير أثناء تحليلها لتدفقات الكائنات وقواميس المراجعات، وتفك تشفير كل كائن تفك ترميزه، بما في ذلك النسخ الأقدم التي تجري عليها المقارنة والعقد الأب التي تمشي عليها

لا يعمل فك التشفير لكل كائن في خريطة المراجعات إلا عندما تكون Password غير فارغة. لذا يجب معاملة الحكم المحسوب على ملف مشفر بكلمة مرور فارغة كحكم غير متحقق منه، حتى لو فُتح المستند نفسه دون مطالبة. خزّن كلمة مرور النموذج بجوار سجل سير العمل ومررها في كل استدعاء

ما لا يغطيه حكم FieldMDP

فحص الحقول جواب بنيوي عن سؤال ضيق واحد، وعدة حالات تقع خارجه. معرفتها مسبقاً هو ما يمنع القيمة 4 من التحول إلى تصعيد دعم فني:

  • لا تجري مقارنة الحقول إلا بعد خروج فحص مستوى المستند نظيفاً وبعد أن غيّرت مراجعة لاحقة واحدة على الأقل أكثر من مواد التحقق أو الأختام الزمنية؛ وخرق DocMDP يُبلَّغ عنه بالقيمة 3 قبل النظر في أي حقل
  • الكائنات المصنفة كمتعلقة بالتوقيع لكنها بلا اسم حقل، مثل قاموس قيمة التوقيع أو قاموس AcroForm معاد الكتابة أو تغيير كتالوج محصور في /AcroForm أو /Perms، تجعل الحكم يعيد 4 بدل التخمين، لذا فالمراجعة التي تضيف توقيع موافقة لاحقاً تحت قفل نشط كثيراً ما تقع على 4
  • لا ينال اسم حقل إلا قواميس الحقول وتعليقات الـ widget؛ وخريطة المراجعات لا تملك صنفاً مخصصاً لتدفقات المظهر (appearance streams)، لذا قد يوقف المحرر الذي يعيد توليد المظاهر مع القيمة عند فحص مستوى المستند بوصفه تغيير محتوى قبل تشغيل فحص الحقول، وسلسلة التفصيل تميز بين الحالتين
  • يتوقف المسح عند أول حقل مقفل أو مدخلاً بلا اسم يقابلهما، لذا فالقيمة 3 تسمي حقلاً مخالفاً واحداً لا كلها
  • تُجمع الأقفال من كل حقل توقيع موقّع دون ترتيبها زمنياً، فالقفل الذي يكتبه موقّع لاحق ينطبق أيضاً على التحديثات السابقة عليه؛ وهذا لا يدفع النتيجة إلا نحو 3 أبداً ولا نحو 2 قط
  • تُقارن الأسماء مطابقة تامة وبحساسية حالة الأحرف، ويتوقف السير عبر /Parent بعد ثمانية مستويات، فالتسلسل العميق بشكل غير معتاد يحل إلى اسم مبتور
  • مدخل FieldMDP الذي لا تنجح بنيته في التحقق ولا يمكن قراءة إجراءه يجعل الحكم يعيد 4

عندما يعود الحكم بالقيمة 4، يمنح GetSignatureRevisionIndex المراجعة التي ينتهي عندها التوقيع، ويسرد GetDocumentRevisionChanges كل كائن أضفته كل مراجعة لاحقة أو عدلته أو حررته مع صنف تغييره. وهذا يكفي ليقرر إنسان الأمر في دقيقة. أما تفاصيل كيفية إلحاق تلك المراجعات فمغطاة في دليل التحديث التدريجي لـ PDFlibPas

FAQ

هل تعني القيمة 2 أن التواقيع صالحة؟

لا. يفحص GetSignatureModificationVerdict ما أُلحق بعد التوقيع ولا يتحقق أبداً من قيمة توقيع CMS أو سلسلة الشهادات أو الإبطال (revocation). القيمة 2 تعني أن التحديثات اللاحقة بقيت داخل الأذونات؛ أما التحقق التشفيري فخطوة منفصلة والاثنان مطلوبان

لماذا ينتج توقيع المعتمد الثاني القيمة 4 بدل 2؟

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

هل يستطيع اسم واحد قفل مجموعة حقول بأكملها؟

نعم. يطابق مدخل القفل الحقل المسمى وكل سليل يبدأ اسمه المؤهل بالكامل به يليه نقطة، فسرد Order في قفل Include يجمد Order.Customer و Order.Total وأي حقل آخر تحت ذلك الأب

أحكام FieldMDP والتوقيع بأقفال الحقول وفحص المراجعات كلها متوفرة في مكتبة PDFlibPas لـ Delphi لملفات PDF، مع ميزات اعتماد DocMDP والتحقق طويل الأمد PAdES التي يعتمد عليها سير عمل الموافقات أعلاه