מאמר טכני

הגנת גיליון XLSX ב-Delphi: 15 אפשרויות הרשאה

אתם מוסרים חוברת עבודה גמורה לעמית ומבקשים ממנו לסנן אותה, לא לשכתב אותה. לכן מגנים על הגיליון. בגרסאות HotXLS ישנות המחווה הזו כתבה דבר אחד בלבד לקובץ: <sheetProtection sheet="1" objects="1" scenarios="1"/>, קשיח ומקובע בכל פעם. הגיליון ננעל, hash הסיסמה צורף, והמשתמש לא יכול היה לעשות דבר, אפילו לא את המיון והסינון שרציתם להשאיר פתוחים. בתיבת הדו-שיח של Excel להגנת גיליון יש חמש עשרה תיבות סימון בדיוק מהסיבה הזאת, והמנוע לא ידע לייצג אף אחת מהן. הפער הזה הוא מה שמודל ההגנה v2.91.0 סוגר

HotXLS הוא רכיב גיליונות עבודה VCL מקורי ל-Delphi ול-C++Builder שקורא וכותב XLS ו-XLSX בלי ש-Excel מותקן. המאמר הזה עוסק בצד ה-XLSX של הגנת גיליון עבודה: ה-enum החדש TXLSXSheetProtectionOption, המאפיין AllowOption שמחליף כל הרשאה בנפרד, וכלל הקידוד היחיד של OOXML שמכשיל כל מי שכותב רכיב <sheetProtection> ביד

מה באמת מגנה הגנת גיליון עבודה

ראשית, הגבול, כי הוא קובע כמה אפשר לסמוך על כל זה. הגנת גיליון בפורמט הגיליונות האלקטרוניים של OOXML ‏(ECMA-376) היא מדיניות אינטראקציה, לא הצפנה. היא אומרת לאפליקציה תואמת אילו עריכות לסרב להן כל עוד הגיליון מוגן. ערכי התאים עדיין יושבים ב-xl/worksheets/sheetN.xml כטקסט רגיל; פתחו את קובץ ה-.xlsx והם שם. הסיסמה האופציונלית נשמרת כ-hash קצר מיושן, לא כמפתח שמבלגן משהו. כל מי שישנה את שם הקובץ, יפתח את החלק, ויסיר את השורה <sheetProtection> יקרא ויערוך הכול

אז ההגנה עונה על "לעצור עמית לעבודה מלהרוס בטעות נוסחה", לא על "לשמור את הנתונים האלה סודיים ממי שממש רוצה אותם". אלה בעיות שונות עם כלים שונים. אם צריך סודיות, רוצים את ההצפנה ברמת חוברת העבודה שמכוסה ב-פלט XLSX מוגן ב-AES, שבאמת מצפין את החבילה. הגנת גיליון והצפנת חוברת עבודה משתלבות היטב, אבל רק השנייה היא מנעול. שימרו על ההבחנה הזאת, ושאר העמוד הוא רק צנרת

חמש עשרה האפשרויות ומאפיין AllowOption

כל גיליון עבודה נושא עכשיו קבוצה של TXLSXSheetProtectionOption שמתארים מה המשתמש עדיין רשאי לעשות בזמן שהגיליון מוגן. האיברים ממופים אחד לאחד אל מאפייני OOXML ואל תיבות הסימון בתיבת הדו-שיח של Excel:

  • xlsxSpoEditObjects, xlsxSpoEditScenarios - עריכת אובייקטי ציור ותרחישי what-if
  • xlsxSpoFormatCells, xlsxSpoFormatColumns, xlsxSpoFormatRows - עיצוב מחדש של תאים, עמודות, שורות
  • xlsxSpoInsertColumns, xlsxSpoInsertRows, xlsxSpoInsertHyperlinks - הוספת עמודות, שורות, קישורים
  • xlsxSpoDeleteColumns, xlsxSpoDeleteRows - מחיקת עמודות, שורות
  • xlsxSpoSelectLockedCells, xlsxSpoSelectUnlockedCells - העברת הבחירה לתאים נעולים או לא נעולים
  • xlsxSpoSort, xlsxSpoAutoFilter, xlsxSpoPivotTables - מיון טווחים, שימוש בתפריטי AutoFilter, עבודה עם PivotTables
  • אתם קוראים וכותבים ביטים בודדים דרך המאפיין המאונדקס AllowOption על TXLSXWorksheet. AllowOption[Opt] = True פירושו שהפעולה מותרת; קביעה שלו ל-False אוסרת אותה. את כל הקבוצה אפשר גם להגיע אליה בבת אחת דרך SheetProtectionOptions, שהיא TXLSXSheetProtectionOptions ‏(סתם set of בפסקל), כך שאפשר לשמור אותה, לשחזר אותה או להחליף אותה כולה

    ברירת המחדל חשובה, ובכוונה: גיליון חדש מתחיל כשכל אפשרות מותרת. הבנאי מאתחל את SheetProtectionOptions עם הטווח המלא, [Low(TXLSXSheetProtectionOption)..High(TXLSXSheetProtectionOption)]. מכאן מצמצמים על ידי הוצאת הפעולות שרוצים לאסור, במקום לבנות סט הרשאות מאפס. הבחירה הזאת היא מה שגורמת לכלל הקידוד של הכותב, בהמשך, להתיישר עם ההתנהגות של Excel

    הגנה על גיליון אבל השארת המיון והסינון פתוחים

    הנה המקרה השכיח מקצה לקצה: מגנים על דוח גמור כך שאי אפשר לעצב מחדש את הפריסה, אבל מאפשרים לקורא למיין ולסנן אותו. שימו לב ש-Protect והאפשרויות עצמאיים. Protect מעביר את הגיליון למצב מוגן ושומר את hash הסיסמה האופציונלי; הוא לא נוגע בקבוצת האפשרויות. אתם מכווננים את AllowOption בנפרד, והמתגים נכנסים לתוקף ברגע שהגיליון מוגן ונשמר

    var
      wb: TXLSXWorkbook;
      sh: TXLSXWorksheet;
    begin
      wb := TXLSXWorkbook.Create;
      try
        sh := wb.Sheets.Add('Protected');
        sh.Cells[1, 1].Value := 'Region'; sh.Cells[1, 2].Value := 'Units';
        sh.Cells[2, 1].Value := 'North';  sh.Cells[2, 2].Value := 120;
        sh.Cells[3, 1].Value := 'South';  sh.Cells[3, 2].Value := 98;
    
        // Protect with a password. This only sets the protected state + hash;
        // the option set is left at its all-permitted default.
        sh.Protect('HotXLS-2026');
    
        // Narrow: keep sort + AutoFilter, forbid reshaping and reformatting.
        sh.AllowOption[xlsxSpoSort]          := True;
        sh.AllowOption[xlsxSpoAutoFilter]    := True;
        sh.AllowOption[xlsxSpoFormatCells]   := False;
        sh.AllowOption[xlsxSpoFormatColumns] := False;
        sh.AllowOption[xlsxSpoFormatRows]    := False;
        sh.AllowOption[xlsxSpoInsertRows]    := False;
        sh.AllowOption[xlsxSpoDeleteRows]    := False;
    
        if wb.SaveAs('protection.xlsx') <> 1 then
          Writeln('SaveAs failed');
      finally
        wb.Free;
      end;
    end;

    שני דברים אפשר לקרוא מהקטע הזה. השורות Sort ו-AutoFilter נכתבות במפורש אף ששתיהן ברירת המחדל היא True; זה תיעוד למתחזק הבא, לא דרישה פונקציונלית. ומכיוון שברירות המחדל נדיבות, השורות היחידות שמשנות את קובץ הפלט הן אלה שמגדירות אפשרות ל-False. זה לא מקרה של ה-API הזה, זה פורמט החיווט של OOXML שנחשף, וזה הסעיף הבא

    כלל הקידוד: השמטה פירושה הרשאה, attr=0 פירושו איסור

    זו העובדה היחידה הלא אינטואיטיבית בכל התכונה, וזה המקום שבו <sheetProtection> שנכתב ביד בדרך כלל טועה. ב-OOXML, כל מאפיין לכל פעולה הוא דגל איסור, והיעדרו הוא הרשאה. מאפיין שחסר פירושו שהפעולה מותרת. מאפיין שנכתב כ-"0" פירושו שהפעולה אסורה כל עוד הגיליון מוגן. אין בקובץ תקין formatCells="1" שפירושו "עיצוב מותר"; פשוט משאירים את המאפיין בחוץ. (ברירת המחדל למאפיין חסר היא ברירת המחדל הבוליאנית של OOXML, true, והמאפיינים האלה נקראים כך ש-"true" פירושו שהעריכה המתאימה מותרת.)

    כותב HotXLS משקף זאת בדיוק. הוא פולט sheet="1" כדי להפעיל הגנה, ואז עובר על קבוצת האפשרויות וכותב attr="0" רק עבור האפשרויות שהגדרתם ל-False. פעולות מותרות לא תורמות דבר לפלט. לכן חוברת העבודה מהסעיף הקודם נרשמת למשהו כזה, כשהיא נושאת רק את הפעולות האסורות בתוספת hash הסיסמה:

    // Conceptual output for the snippet above (attributes elided for brevity):
    // <sheetProtection sheet="1"
    //   formatCells="0" formatColumns="0" formatRows="0"
    //   insertRows="0" deleteRows="0"
    //   password="...4-hex..."/>
    // Note what is NOT there: no sort, no autoFilter, no selectLockedCells.
    // Their absence is exactly what tells Excel those actions stay allowed.

    אם הגעתם מהמחרוזת הישנה והקשיחה וציפיתם לראות כל מאפיין מפורש, זה נראה דליל, כמעט שגוי. אבל זה נכון. קובץ שהיה מפרט sort="1" ו-autoFilter="1" היה אומר אותו דבר לקורא תואם, אבל Excel עצמו כותב את הצורה המינימלית של איסור בלבד, וההתאמה אליה שומרת על diffs קטנים ועל round-trips משעממים. המאפיינים objects ו-scenarios פועלים לפי אותו כלל: הם מותרים כברירת מחדל, ולכן הם מופיעים כ-"0" רק כשאתם אוסרים אותם, בניגוד ל-objects="1" scenarios="1" הישן שנכתב תמיד

    קריאת ההגנה בחזרה, נאמנות round-trip

    מודל הרשאות שאפשר לכתוב אבל לא לקרוא הוא דלת חד-כיוונית, והסימפטום הרגיל הוא מחזור טעינה-עריכה-שמירה שמרחיב בשקט את ההרשאות. HotXLS סוגר את זה. כאשר ParseWorksheetXml פוגש רכיב <sheetProtection> הוא מסמן את הגיליון כמוגן, קולט את hash הסיסמה אם יש כזה, ואז מפענח כל מאפיין לכל פעולה בחזרה אל AllowOption באותה מוסכמה בכיוון ההפוך: מאפיין שקיים ושווה ל-"0" אוסר את הפעולה; מאפיין חסר משאיר את האפשרות בברירת המחדל המותרת שלה

    var
      wb: TXLSXWorkbook;
      sh: TXLSXWorksheet;
    begin
      wb := TXLSXWorkbook.Create;
      try
        wb.LoadFromFile('protection.xlsx');
        sh := wb.Sheets[1];                  // XLSX sheets are 1-based
        if sh.IsProtected then
        begin
          Writeln('Protected; password hash present: ',
            sh.SheetProtectHash <> '');
          Writeln('Sort allowed:       ', sh.AllowOption[xlsxSpoSort]);
          Writeln('AutoFilter allowed: ', sh.AllowOption[xlsxSpoAutoFilter]);
          Writeln('FormatCells allowed:', sh.AllowOption[xlsxSpoFormatCells]);
        end;
      finally
        wb.Free;
      end;
    end;

    טענו את הקובץ שהכותב יצר, ותקבלו את Sort ו-AutoFilter חזרה כ-True, ואת FormatCells כ-False, הקבוצה ששמרתם, שלמה. הסימטריה הזאת היא כל העניין: ערכו תא אחד בגיליון מוגן וחלקית מותר, ושמרו שוב, וארבע עשרה ההרשאות שלא נגעתם בהן ישרדו במקום להתמוטט בחזרה לברירת המחדל הישנה של הכול או כלום

    הערות מעשיות ומגבלות

    כמה דברים שכדאי לדעת לפני שמחברים את זה לצינור דיווח:

  • הסיסמה חלשה לפי התכנון הגנת גיליון XLSX שומרת hash מיושן בן 16 ביט, אותו אחד ש-Excel משתמש בו כבר עשרות שנים, והוא נשמר כאן לטובת תאימות. הוא מרתיע עריכות מקריות, לא עומד מול תוקף. אל תתייחסו אליו כשומר סודות. להגנה אמיתית, הצפינו את חוברת העבודה
  • קביעת אפשרויות לפני ההגנה היא בסדר AllowOption אפשר להקצות בין אם הגיליון מוגן כרגע ובין אם לא; המתגים פשוט מתארים מה ההגנה תאפשר ברגע ש-Protect ייכנס לפעולה. UnProtect מנקה את המצב המוגן ואת ה-hash אבל משאיר את קבוצת האפשרויות שלכם במקומה לפעם הבאה
  • סמנטיקת תאים נעולים עדיין חלה ההגנה חוסמת רק עריכות לתאים שמאפיין Locked שלהם מוגדר, ברירת המחדל של חוברת העבודה. השארת אזור קלט ניתן לעריכה היא עבודה של סגנון התא, לא אפשרות הגנה; שתי השכבות משתלבות כמו ב-Excel
  • זהו מנוע ה-XLSX מודל האפשרויות משקף את המאפיינים הישנים יותר של מנוע ה-XLS מסוג Allow*, אבל שמות ה-enum והמאפיינים כאן (xlsxSpo*, AllowOption) שייכים ל-TXLSXWorksheet ב-lxHandleX. אם אתם גם מפעילים פריסת הדפסה על אותם גיליונות, ההדרכה על הגנה והגדרת עמודים מסבירה איך ההגדרות האלה יושבות לצד אזורי הדפסה וכותרות, ואימות נתונים, AutoFilter וטבלאות משתלב באופן טבעי עם השארת xlsxSpoAutoFilter פתוח בדוח נעול
  • מודל ההגנה המפורט ושאר מנוע הקריאה והכתיבה של XLSX נשלחים ב-HotXLS Component עבור Delphi ו-C++Builder; דף המוצר כולל את ממשק ה-API המלא של הגיליון, כולל ההפניה המלאה לאפשרויות ההגנה