مقال تقني

دوال رد نداء الصيغ الخطرة في HotXLS: قصر CALL وWEBSERVICE

يرفض HotXLS توجيه 20 اسم صيغة خطير، منها CALL وREGISTER.ID وWEBSERVICE وDDE، إلى ردود نداء دوال المستخدم في دلفي ما لم تفعل ذلك بنفسك. الخاصية AllowUnsafeFormulaCallbacks في المصنف افتراضها False، والفحص يجري قبل تقييم أي وسيطة، والاستدعاء المرفوض يبلغ xlfeUnsafeFunctionDenied دون استدعاء معالج واحد

السيناريو الذي جعل هذا ضروريًا دنيوي تمامًا. خدمة تقبل ملفات XLS أو XLSX مرفوعة، وتعيد حسابها على الخادم وتقرأ بضعة مجاميع منها. سجل التطبيق المضيف معالج OnUserFunction منذ سنوات لبضعة دوال أعمال، وفي مكان ما من الطريق نما بذلك المعالج فرع يشمل كل ما عدا، يعيد توجيه أي شيء لا يعرفه إلى جدول إضافات. لم يكتب أحد في الفريق قط =WEBSERVICE(...) في خلية. المرفع فعل. الإبقاء على تلك الصيغة سليمة عبر الفتح وإعادة الحساب والحفظ ميزة وفاء للملف، أما السماح لها ببلوغ كود مضيف قادر على فتح sockets أو ملفات فقرارة تفويض، وقبل أن يفصل HotXLS بين الاثنين كانت المكتبة تتخذ ذلك القرار بهدوء نيابة عنك

كيف تحول حفظ صيغة إلى إذن بتنفيذها؟

السبب الجذري كان مسار احتياطي واحدًا. يفكك HotXLS كل اسم دالة Excel يعرفه، لكن ليس لكل اسم معروف تنفيذ في محرك الحساب. كانت الدوال المدمجة المعروفة غير المنفذة تسقط في الاحتياطي نفسه لدوال المستخدم المعرفة الذي تسقط فيه الأسماء المخصصة حقًا، فتقاسم CALL وREGISTER.ID مسار التوجيه مع DISCOUNT أو REGIONRATE خاصتك. وكانت الأسماء المجهولة مثل WEBSERVICE أو DDE يمكنها بالمثل أن تطابق مدخلًا يحمل الاسم نفسه في سجل المصنف أو السجل على مستوى العملية أو معالج حدث. ميكانيكا ذلك الاحتياطي مغطاة في كيف يحل HotXLS الدوال المخصصة عبر OnUserFunction؛ والمشكلة أن شيئًا على ذلك المسار لم يسأل هل الاسم نفسه من الأسماء التي ينبغي لمضيف عاقل أن ينفذها أصلًا

ترتيب التوجيه يهم لمعرفة ما تعنيه «المجهول» هنا. الاستدعاء الذي لا يستطيع المحرك تقييمه أصلًا يعرض بالتناوب على ربطات LAMBDA وLET المعجمية، التي تحلها أولًا ميزة closures في محرك صيغ HotXLS، ثم على الدوال المحلية للمصنف المسجلة بـ RegisterUserFunction، ثم على الدوال على مستوى العملية من TXLSWorkbook.RegisterGlobalUserFunction، وأخيرًا على حدثي OnUserFunction وOnUserFunctionEx. وحين يرفض الجميع فقط تصير الدالة المجهولة حقًا قيمة #NAME?. وكل مرحلة بعد بحث الـ lambda تسلم التحكم إلى كود كتبته أنت، وهذا بالضبط سبب وجوب جلوس فحص الأمان أمام السلسلة كلها لا داخل أي معالج منها

كيف يقصر HotXLS ردود نداء الصيغ الخطرة: يفحص GetValueItemUserFunction الاسم قبل بناء مصفوفة الوسائط أو تشغيل أي محلل، فتخرج =WEBSERVICE(AUDIT_TOKEN()) المتداخلة بقيمة lxErrorUnsafeFunctionDenied ويبقى سجل التدقيق فارغًا، بينما تسلك الاستدعاءات الآمنة السلسلة من ربطات LAMBDA وLET حتى OnUserFunction
عندما ترفض كل المراحل فقط تصير الدالة المجهولة حقًا قيمة #NAME?، ولهذا يجلس فحص الأمان أمام السلسلة كلها لا داخل أي معالج منها

أي أسماء دوال يحجب HotXLS افتراضيًا؟

تحتفظ XLSFormulaCallbackIsUnsafe في lxCalc.pas بمجموعة رفض ثابتة من 20 اسمًا: DDE وCALL وREGISTER وREGISTER.ID وWEBSERVICE وRTD وSQL.REQUEST وEXEC وRUN وCREATE.OBJECT وAPP.ACTIVATE وSEND.KEYS وOPEN وSAVE وSAVE.AS وFOPEN وFWRITE وFWRITELN وFCLOSE وFILE.DELETE. إنها الأسماء التي، في Excel أو لغة macros لديه، تحمل كودًا أصليًا أو تصل إلى الشبكة أو تتكلم مع عمليات أخرى أو تلمس نظام الملفات. وقبل المقارنة تقص الدالة الفراغات المحيطة، وترفع الاسم إلى الأحرف الكبيرة، وتنزع بادئة _XLFN. أو _XLWS. واحدة، فيصطاد _xlfn.webservice المكتوب بإصدار Excel أحدث مثل الإملاء العاري تمامًا. وتسكن القائمة عند حدود الحاسبة لا في مفككات Classic وXLSX وODS، فيبقى AST واحد وتدفق رموز BIFF واحد ومصنف محول واحد يتصرفون بالطريقة ذاتها

كيف يضبط HotXLS اسم دالة قبل مقارنة رد النداء الخطرة: تقص الفراغات، ويرفع الاسم إلى الأحرف الكبيرة، وتنزع بادئة _XLFN. أو _XLWS. واحدة فيصطاد _xlfn.webservice مثل الإملاء العاري، ثم يطابق الناتج تطابقًا تامًا مع مجموعة الرفض الثابتة البالغة 20 اسمًا في lxCalc.pas
تمتد القائمة على الأسماء التي تحمل كودًا أصليًا أو تصل إلى الشبكة أو تتكلم مع عمليات أخرى أو تلمس نظام الملفات، من DDE وCALL وWEBSERVICE إلى FWRITE وFILE.DELETE

حافتان تستحقان المعرفة قبل أن تعتمد عليها. المطابقة تامة، فمعالج تسجله باسم MYWEBSERVICE لا يتأثر، وبالعكس UDF داخلي مشروع صادف أن اسمه OPEN أو RUN صار يرفض افتراضيًا. ومجموعة الرفض ليست sandbox لمعالجاتك أنت أيضًا. إن كان فرعك الشامل ينفذ أسماء إضافات عشوائية، فالبوابة توقف الخطرة المشهورة فقط ولا شيء غيرها؛ والإصلاح الدائم ما يزال معالجًا يطابق قائمة سماح صريحة بـ SameText ويترك Handled على False لكل ما لا يملكه

لماذا يجب أن تجري البوابة قبل تقييم الوسائط؟

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

procedure TImportService.HandleUdf(Sender: TObject;
  const FunctionName: WideString; const Args: Variant;
  var Value: Variant; var Handled: Boolean);
begin
  if SameText(FunctionName, 'AUDIT_TOKEN') then
  begin
    FAuditLog.Add('AUDIT_TOKEN evaluated');   // أثر جانبي في كود المضيف
    Value := 'token-42';
    Handled := True;
  end;
end;

Book.OnUserFunction := HandleUdf;
Eval := Sheet.EvaluateFormulaAt(1, 1, '=WEBSERVICE(AUDIT_TOKEN())');
// Eval.Status = xlfeUnsafeFunctionDenied وEval.Value = Null،
// وEval.Issue.NativeCode = -106، وما يزال FAuditLog فارغًا

افتراضي المصنف مقابل خيار TXLSFormulaEvaluationOptions لكل استدعاء

علم المصنف هو الافتراضي وخيار الاستدعاء الواحد هو الكلمة الأخيرة. يحكم TXLSWorkbook.AllowUnsafeFormulaCallbacks وTXLSXWorkbook.AllowUnsafeFormulaCallbacks إعادة الحساب العادية وCalculate وEvaluateFormulaAt ذات الوسيطتين وقوالب التقييم والعروض للقراءة فقط، وفي XLSX كل worker في مجمع إعادة الحساب المتوازي. وأي نقطة دخول تقبل سجل TXLSFormulaEvaluationOptions صريحًا تأخذ Options.AllowUnsafeFormulaCallbacks حكمًا لذلك الاستدعاء ولا تجمعه OR مع خاصية المصنف. تلك اللاتماثل مقصود: عمل داخلي موثوق يستطيع تفويض بحث RTD واحد دون قلب المصنف كله، ومصنف مفعل عالميًا يستطيع أن يجبر تقييمًا حساسًا على العودة إلى الرفض

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // المصنف يبقى مقفلًا، ويجوز استدعاء موثوق واحد
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // المصنف مفعل، لكن هذا التقييم لنص مرفوع ليس كذلك
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // العلم False من جديد
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

قلب خاصية المصنف يمس أيضًا رسم التبعيات بوسمها متسخة في كلا المحركين. بلا تلك الخطوة كان يمكن أن يقدم نتيجة مخزنة حسبت والردود مسموحة بعد سحب الإذن، أو أن تعيش نتيجة مخزنة بقيمة xlfeUnsafeFunctionDenied أطول من التفعيل. ألحق الحالة الجديدة بـ TXLSFormulaEvaluationStatus بعد xlfeFailed، فلها الترتيبي 10 ويحتفظ كل ترتيبي موجود بقيمته؛ وتنطبق قاعدة الإلحاق في الذيل نفسها على حقل سجل الخيارات وعلى getter وsetter في IXLSWorkbook، مع أن مستهلكًا بني مقابل إصدار أقدم يحتاج مع ذلك إلى إعادة ترجمة

ماذا يحدث لنص الصيغ المجهولة والخطرة عند الحفظ؟

حفظ الصيغة وتنفيذها صارا سؤالين منفصلين، وسياسة الدخول تجيب عن الأول فقط. تحمل FormulaEntryPolicy في أي صنف من أصناف المصنف UnknownFunctionMode وUnknownNameMode، وكلاهما افتراضه xlfusmReject، فإسناد صيغة تحمل استدعاءً مجهولًا عبر خاصية Formula العادية يرفض قبل أن تتغير قيمة الخلية أو مخزن الصيغ أو التبعيات. وValidateFormulaEntry تبلغ القرار نفسه بلا آثار جانبية. أما المسارات الموثوقة كتحميل الملفات والنسخ وتحويل التنسيق فتتجاوز سياسة دخول المستخدم تلك، لأن افتراضيًا صارمًا يجب ألا يرفض أبدًا رموزًا حاضرة سلفًا في ملف تفتحه فحسب

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // دخول توافقية
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // مخزنة، غير مفوضة
  Book.SaveAs('rates.xls');
end;

في BIFF8 الكلاسيكي لا يملك الاستدعاء المجهول رمزًا خاصًا به، فيكتبه HotXLS بالطريقة التي يكتب بها Excel دوال الإضافات. تحصل الصيغة على رمز PtgNameX‏ ($59) يشير مدخل XTI الخاص به إلى SUPBOOK الإضافات مع ضبط كلا مؤشري الورقة إلى $FFFE، يليه رموز الوسائط وPtgFuncVar حاملة رقم الدالة 255 وعدد وسائط يشمل خانة الاسم. وجسم ExternName الداعم هو ست بايتات معدومة، وبايت طول وعلم Unicode، واسم الدالة بترميز UTF-16، ثم صيغة من بايتتين $1C $17، أي PtgErr يحمل #REF!. ويرفض الكاتب الأسماء الأطول من 255 محرفًا، وأكثر من 29 وسيطة، والوجهة BIFF5. وكيف يصنف HotXLS مداخيل SUPBOOK الإضافية تلك بجوار روابط المصنفات الخارجية يشرحه مقال قواعد تصنيف SUPBOOK وXTI لروابط BIFF الخارجية. ويحفظ XLSX نص الدالة الخام ويحفظ ODS صيغته msoxl:، وفي كل تنسيق يعاد فتح ملف حفظ =WEBSERVICE(...) بالنص سليمًا ويواصل تقييمه إلى xlfeUnsafeFunctionDenied افتراضيًا

كيف يكتب HotXLS استدعاء صيغة مجهولًا في BIFF8 الكلاسيكي: تحمل الصيغة رمز PtgNameX يشير مدخل XTI فيه إلى SUPBOOK الإضافات بكلا مؤشري الورقة $FFFE، ثم رموز الوسائط وPtgFuncVar برقم دالة 255، مدعومة بجسم ExternName ينتهي بـ PtgErr من بايتتين $1C $17 يحمل #REF!
يحفظ XLSX نص الدالة الخام ويحفظ ODS صيغة msoxl: لديه، فملف حفظ =WEBSERVICE(...) يعاد فتحه بالنص سليمًا ويواصل التقييم إلى xlfeUnsafeFunctionDenied افتراضيًا

إن كان خط معالجتك يقيّم مصنفات لم يؤلفها، فاترك AllowUnsafeFormulaCallbacks على False، وأبق المعالجات على قائمة سماح صريحة، وامنح خيارات الاستدعاء الواحد حيث يكون مصدر الصيغة منك فقط. وAPI ردود النداء وسياسة الدخول والتقييم كامل موثق مع مكوّن HotXLS للجداول لدلفي