Техническа статия

Защита на XLSX работен лист в Delphi: 15 Allow опции

Подавате готов workbook на колега и му казвате да го филтрира, не да го пренаписва. Затова защитавате sheet-а. В по-старите HotXLS builds този жест записваше едно нещо във файла: <sheetProtection sheet="1" objects="1" scenarios="1"/>, hard-coded, всеки път. Sheet-ът се заключваше, password hash-ът се добавяше и потребителят не можеше да прави нищо, дори sort и filter, които всъщност искахте да оставите отворени. Собственият диалог "Protect Sheet" на Excel има петнадесет отметки точно по тази причина, а engine-ът не можеше да изрази нито една от тях. Тази празнина затваря protection моделът във v2.91.0

HotXLS е нативен VCL spreadsheet компонент за Delphi и C++Builder, който чете и записва XLS и XLSX без да автоматизира самия Excel. Тази статия е за XLSX страната на worksheet protection: новия TXLSXSheetProtectionOption enum, AllowOption property-то, което превключва всяко право, и правилото за OOXML encoding, което хваща всеки, който пише <sheetProtection> елемент на ръка

Какво всъщност защитава worksheet protection

Първо, границата, защото тя решава колко от всичко това трябва да ви вдъхва доверие. Worksheet protection в OOXML spreadsheet format (ECMA-376) е политика за взаимодействие, не encryption. Тя казва на съвместимото приложение кои edits да откаже, докато sheet-ът е защитен. Cell стойностите все още стоят в xl/worksheets/sheetN.xml на plain text; разархивирайте .xlsx и те са си там. Опционалният password се съхранява като кратък legacy hash, не като key, който разбърква нещо. Всеки, който преименува файла, отвори part-а и махне <sheetProtection> реда, може да чете и редактира всичко

Така че protection отговаря на „спри колегата ми да съсипе случайно една формула“, не на „скрий тези данни от някой мотивиран“. Това са различни проблеми с различни инструменти. Ако ви трябва confidentiality, искате workbook-level encryption, описано в AES-protected XLSX output, което наистина encipher-ва пакета. Sheet protection и workbook encryption се комбинират чисто, но само второто е lock. Дръжте тази линия ясна и останалото на тази страница е просто plumbing

Петнадесетте опции и AllowOption property-то

Всеки worksheet вече носи набор от TXLSXSheetProtectionOption values, описващи какво потребителят още може да прави, докато sheet-ът е protected. Членовете map-ват едно към едно към OOXML attributes и към Excel dialog checkboxes:

  • xlsxSpoEditObjects, xlsxSpoEditScenarios — редактиране на drawing objects и what-if scenarios
  • xlsxSpoFormatCells, xlsxSpoFormatColumns, xlsxSpoFormatRows — форматиране на клетки, колони и редове
  • xlsxSpoInsertColumns, xlsxSpoInsertRows, xlsxSpoInsertHyperlinks — вмъкване на колони, редове и links
  • xlsxSpoDeleteColumns, xlsxSpoDeleteRows — изтриване на колони и редове
  • xlsxSpoSelectLockedCells, xlsxSpoSelectUnlockedCells — преместване на селекцията върху locked или unlocked клетки
  • xlsxSpoSort, xlsxSpoAutoFilter, xlsxSpoPivotTables — сортиране на ranges, използване на AutoFilter dropdowns и работа с PivotTables

Четете и записвате отделните bits през индексираното AllowOption property на TXLSXWorksheet. AllowOption[Opt] = True означава, че действието е разрешено; задаването му на False го забранява. Целият набор е достъпен и наведнъж през SheetProtectionOptions, TXLSXSheetProtectionOptions (обикновен Pascal set of), така че можете да го запишете, възстановите или замените изцяло

Подразбиращото се поведение е важно и е умишлено: току-що създаден worksheet започва с позволена всяка опция. Конструкторът seed-ва SheetProtectionOptions с пълния диапазон, [Low(TXLSXSheetProtectionOption)..High(TXLSXSheetProtectionOption)]. Оттам стеснявате, като изключвате действията, които искате да забраните, вместо да строите permission set от нулата. Този избор е това, което кара правилото за encoding по-долу да съвпада с поведението на Excel

Защита на sheet, но оставяне на sort и filter отворени

Ето често срещаният случай от край до край: защитете завършен report, така че оформлението да не може да бъде пренаредено, но оставете читателя да го сортира и филтрира. Забележете, че Protect и опциите са независими. Protect превключва sheet-а в protected state и записва optional password hash-а; не пипа option set-а. Настройвате AllowOption отделно, а toggles-ите влизат в сила, когато sheet-ът е защитен и записан

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;

Две неща да прочетете от този snippet. Sort и AutoFilter линиите се записват изрично, въпреки че и двете по подразбиране са True; това е документация за следващия maintainer, не функционално изискване. И понеже defaults-ите са permissive, единствените линии, които променят output файла, са тези, които задават опция на False. Това не е случайност на този API, а OOXML wire format-ът, който прозира, и това води към следващия section

Правилото за encoding: omit означава allow, attr=0 означава forbid

Това е единственият counter-intuitive факт в цялата функция и именно тук ръчно написаният <sheetProtection> обикновено греши. В OOXML всеки per-action attribute е forbid flag, а липсата му е permission. Attribute, който липсва, означава, че действието е allowed. Attribute, записан като "0", означава, че действието е forbidden, докато sheet-ът е protected. Няма formatCells="1" в добре оформен файл, който да означава "formatting is allowed"; просто не включвате атрибута. (Подразбирането за липсващ attribute е OOXML boolean default true, а тези атрибути са именувани така, че "true" да означава, че съответното edit е permitted.)

Writer-ът на HotXLS отразява това точно. Той изписва sheet="1" за да включи защитата, после обхожда option set-а и пише attr="0" само за опциите, които сте задали на False. Permitted действията не допринасят нищо към output-а. Така workbook-ът от предишния section се сериализира до нещо подобно, носейки само forbidden действията плюс password 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.

Ако идвате от стария hard-coded string и очаквате да видите всеки attribute изписан, това изглежда оскъдно, почти грешно. Но е правилно. Файл, който изброява sort="1" и autoFilter="1" би означавал същото за съвместим reader, но самият Excel пише минималната forbid-only форма и това държи diff-овете малки, а round-trip-овете скучни. objects и scenarios атрибутите следват идентичното правило: те са default-permitted, така че се появяват само като "0" когато ги забраните, което е обратното на стария objects="1" scenarios="1" който се изписваше безусловно

Четене на protection обратно: round-trip fidelity

Permission модел, който можете да запишете, но не и да прочетете, е еднопосочна врата, а обичайният симптом е load-edit-save цикъл, който тихо разширява разрешенията. HotXLS затваря това. Когато ParseWorksheetXml срещне <sheetProtection> елемент, той маркира sheet-а protected, прихваща password hash-а, ако има такъв, и после декодира всеки per-action attribute обратно в AllowOption с една и съща конвенция наобратно: attribute, който присъства и е равен на "0", забранява действието; липсващ attribute оставя опцията в нейното permitted default състояние

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;

Заредете файла, който writer-ът е произвел, и получавате Sort и AutoFilter обратно като True, FormatCells като False - наборът, който сте записали, непокътнат. Тази симетрия е цялата идея: редактирате една клетка в защитен, частично permitted sheet и записвате отново, а четиринадесетте permissions, които не сте пипнали, оцеляват вместо да се сринат обратно към стария all-or-nothing default

Практически бележки и ограничения

Няколко неща, които си струва да знаете, преди да включите това в report pipeline:

  • Password-ът е weak по дизайн. XLSX worksheet protection съхранява 16-битов legacy hash (същия, който Excel използва от десетилетия), пазен тук за interoperability. Той възпира случайни edits; не се противопоставя на attacker. Не го третирайте като secret-keeper. За истинска защита encrypt-нете workbook-а
  • Задаването на options преди защитата е наред. AllowOption може да се задава независимо дали sheet-ът в момента е protected; toggles-ите просто описват какво protection ще позволи, когато Protect е в сила. UnProtect изчиства protected state-а и hash-а, но оставя option set-а на място за следващия път
  • Locked-cell semantics все още важат. Protection блокира само edits към клетки, чийто Locked attribute е зададен (workbook default-ът). Да оставите input region editable е работа на cell-style-а, не на protection option; двете layers се комбинират по същия начин, както в Excel
  • Това е XLSX engine-ът. Option model-ът отразява по-старите Allow* properties на XLS engine-а, но enum и property имената тук (xlsxSpo*, AllowOption) принадлежат на TXLSXWorksheet в lxHandleX. Ако управлявате и печатното оформление на същите sheet-ове, protection and page-setup walkthrough показва как тези settings стоят до print areas и headers, а data validation, AutoFilter and tables се съчетава естествено с оставянето на xlsxSpoAutoFilter отворен върху заключен report

Fine-grained protection model-ът и останалата XLSX read/write engine част идват в HotXLS Component за Delphi и C++Builder; product page-ът носи пълния worksheet API, включително пълната reference за protection options