Articol tehnic

Protecția foii de calcul XLSX în Delphi: 15 opțiuni Allow

Îi dai unui coleg un workbook finalizat și îi ceri să îl filtreze, nu să îl rescrie. Așa că protejezi foaia. În versiunile mai vechi de HotXLS, acest gest scria un singur lucru în fișier: <sheetProtection sheet="1" objects="1" scenarios="1"/> codificat fix, de fiecare dată. Foaia se bloca, hash-ul parolei era atașat, iar utilizatorul nu putea face nimic, nici măcar sortarea și filtrarea pe care voiai de fapt să le lași deschise. Dialogul „Protect Sheet” din Excel are cincisprezece casete de selectare exact din acest motiv, iar motorul nu putea exprima niciuna dintre ele. Această lacună este acoperită de modelul de protecție din v2.91.0

HotXLS este o componentă nativă VCL pentru foi de calcul în Delphi și C++Builder care citește și scrie XLS și XLSX fără a avea Excel instalat. Acest articol este despre partea XLSX a protecției foii de calcul: noul TXLSXSheetProtectionOption enum, AllowOption proprietatea care comută fiecare permisiune și regula unică de codificare OOXML care îi încurcă pe toți cei care scriu manual un <sheetProtection> element

Ce protejează de fapt protecția foii de calcul

Mai întâi, limita, pentru că ea decide câtă încredere ar trebui să acorzi tuturor acestor lucruri. Protecția foii de calcul în formatul OOXML (ECMA-376) este o politică de interacțiune, nu criptare. Ea îi spune unei aplicații conforme ce editări să refuze cât timp foaia este protejată. Valorile celulelor sunt încă stocate în xl/worksheets/sheetN.xml în text simplu; dezarhivează .xlsx și ele sunt chiar acolo. Parola opțională este stocată ca un hash scurt moștenit, nu ca o cheie care să amestece ceva. Oricine redenumește fișierul, deschide partea și elimină linia <sheetProtection> citește și editează totul

Așadar, protecția răspunde la „oprește-l pe colegul meu să nu strice din greșeală o formulă”, nu la „păstrează aceste date secrete de cineva hotărât”. Sunt probleme diferite, cu instrumente diferite. Dacă ai nevoie de confidențialitate, vrei criptarea la nivel de workbook, descrisă în ieșire XLSX protejată cu AES, care chiar criptează pachetul. Protecția foii și criptarea workbook-ului se combină bine, dar doar a doua este un lacăt. Ține această distincție clară și restul paginii este doar infrastructură

Cele cincisprezece opțiuni și proprietatea AllowOption

Fiecare foaie de calcul poartă acum un set de TXLSXSheetProtectionOption valori care descriu ce mai poate face utilizatorul cât timp foaia este protejată. Membrii corespund unu la unu cu atributele OOXML și cu casetele de selectare din dialogul Excel:

  • xlsxSpoEditObjects, xlsxSpoEditScenarios - editează obiecte de desen și scenarii what-if
  • xlsxSpoFormatCells, xlsxSpoFormatColumns, xlsxSpoFormatRows - reformatează celulele, coloanele, rândurile
  • xlsxSpoInsertColumns, xlsxSpoInsertRows, xlsxSpoInsertHyperlinks - inserează coloane, rânduri, legături
  • xlsxSpoDeleteColumns, xlsxSpoDeleteRows - șterge coloane, rânduri
  • xlsxSpoSelectLockedCells, xlsxSpoSelectUnlockedCells - mută selecția pe celule blocate sau deblocate
  • xlsxSpoSort, xlsxSpoAutoFilter, xlsxSpoPivotTables - sortează intervale, folosește meniurile derulante AutoFilter și lucrează cu PivotTables

Citești și scrii biți individuali prin proprietatea indexată AllowOption a TXLSXWorksheet. AllowOption[Opt] = True înseamnă că acțiunea este permisă; setarea lui False o interzice. Întregul set poate fi accesat și dintr-o dată prin SheetProtectionOptions, un TXLSXSheetProtectionOptions (un simplu Pascal set of), astfel încât îl poți salva, restaura sau înlocui integral

Valoarea implicită contează și este intenționată: o foaie de calcul creată de curând pornește cu fiecare opțiune permisă. Constructorul inițializează SheetProtectionOptions cu intervalul complet, [Low(TXLSXSheetProtectionOption)..High(TXLSXSheetProtectionOption)]. De acolo restrângi prin excluderea acțiunilor pe care vrei să le interzici, în loc să construiești de la zero un set de permisiuni. Această alegere face ca regula de codare a scriitorului, de mai jos, să se alinieze cu comportamentul Excel

Protejarea unei foi, dar lăsarea sortării și filtrării deschise

Iată cazul obișnuit, de la un capăt la altul: protejează un raport finalizat astfel încât aspectul să nu poată fi reconfigurat, dar permite cititorului să îl sorteze și să îl filtreze. Observă că Protect și opțiunile sunt independente. Protect comută foaia în starea protejată și stochează hash-ul opțional al parolei; nu atinge setul de opțiuni. Ajustezi AllowOption separat, iar comutatoarele intră în vigoare odată ce foaia este protejată și salvată

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;

Două lucruri de reținut din acel fragment. Sort și AutoFilter liniile sunt scrise explicit, deși ambele au implicit valoarea True; asta este documentație pentru următorul întreținător, nu o cerință funcțională. Și, deoarece valorile implicite sunt permisive, singurele linii care schimbă fișierul de ieșire sunt cele care setează o opțiune la False. Acesta nu este un accident al acestei API, ci formatul de transport OOXML care se vede, ceea ce va fi subiectul secțiunii următoare

Regula de codare: omisiunea înseamnă permis, attr=0 înseamnă interzis

Acesta este singurul fapt contraintuitiv din întreaga funcționalitate și locul în care <sheetProtection> scris de mână greșește de obicei. În OOXML, fiecare atribut specific acțiunii este un interzicere indicator, iar absența lui înseamnă permisiune. Un atribut lipsă înseamnă că acțiunea este permisă. Un atribut scris ca "0" înseamnă că acțiunea este interzisă cât timp foaia este protejată. Nu există formatCells="1" într-un fișier bine format pentru a însemna „formatarea este permisă”; pur și simplu lași atributul deoparte. (Valoarea implicită pentru un atribut absent este valoarea booleană OOXML true, iar aceste atribute sunt denumite astfel încât „true” să însemne că editarea corespunzătoare este permisă.)

Scriitorul HotXLS reproduce exact asta. El emite sheet="1" pentru a activa protecția, apoi parcurge setul de opțiuni și scrie attr="0" doar pentru opțiunile pe care le setezi la False. Acțiunile permise nu contribuie cu nimic la ieșire. Așadar, registrul de lucru din secțiunea anterioară se serializează cam așa, purtând doar acțiunile interzise plus hash-ul parolei:

// 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.

Dacă veneai de la vechiul șir codificat manual și te așteptai să vezi fiecare atribut scris explicit, asta pare sărac, aproape greșit. Este corect. Un fișier care ar lista sort="1" și autoFilter="1" ar însemna același lucru pentru un cititor conform, dar Excel însuși scrie forma minimă, doar cu interdicții, iar alinierea la ea păstrează diff-urile mici și round-trip-urile lipsite de surprize. objects și scenarios atribute urmează aceeași regulă: sunt permise implicit, așa că apar doar ca "0" când le interzici, ceea ce este invers față de vechiul objects="1" scenarios="1" care era emis necondiționat

Citirea înapoi a protecției: fidelitate la round-trip

Un model de permisiuni pe care îl poți scrie, dar nu citi, este o stradă cu sens unic, iar simptomul obișnuit este un ciclu încărcare-editare-salvare care lărgește în tăcere permisiunile. HotXLS elimină asta. Când ParseWorksheetXml întâlnește un <sheetProtection> element, setează foaia ca protejată, capturează hash-ul parolei dacă este prezent și apoi decodează fiecare atribut specific acțiunii înapoi în AllowOption folosind aceeași convenție în sens invers: un atribut prezent și egal cu "0" interzice acțiunea; un atribut absent lasă opțiunea la valoarea ei implicită permisă

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;

Încarcă fișierul produs de scriitor și obții Sort și AutoFilter înapoi ca True, FormatCells ca False setul pe care l-ai salvat, intact. Această simetrie este exact ideea: modifici o singură celulă într-o foaie protejată, cu permisiuni parțiale, și o salvezi din nou, iar cele paisprezece permisiuni pe care nu le-ai atins supraviețuiesc în loc să revină la vechiul comportament implicit totul sau nimic

Note practice și limite

Câteva lucruri utile de știut înainte să integrezi asta într-un flux de rapoarte:

  • Parola este slabă prin design. XLSX worksheet protection stochează un hash legacy pe 16 biți (același pe care Excel îl folosește de zeci de ani), păstrat aici pentru interoperabilitate. Descurajează modificările accidentale; nu rezistă unui atacator. Nu o trata ca pe un mecanism de păstrare a secretelor. Pentru protecție reală, criptează registrul de lucru
  • Setarea opțiunilor înainte de protejare este în regulă. AllowOption poate fi atribuit indiferent dacă foaia este sau nu protejată în prezent; comutatoarele descriu pur și simplu ce va permite protecția după ce Protect intră în vigoare. UnProtect șterge starea protejată și hash-ul, dar lasă setul tău de opțiuni la locul lui pentru data viitoare
  • Semantica celulelor blocate se aplică în continuare. Protecția blochează doar editările celulelor al căror Locked este setat (valoarea implicită a registrului de lucru). Să lași o zonă de introducere editabilă ține de stilul celulei, nu de o opțiune de protecție; cele două straturi se combină la fel ca în Excel
  • Acesta este motorul XLSX. Modelul de opțiuni oglindește vechile proprietăți Allow* ale motorului XLS, dar numele enumului și ale proprietăților de aici (xlsxSpo*, AllowOption) aparțin lui TXLSXWorksheet în lxHandleX. Dacă gestionezi și aspectul la imprimare pe aceleași foi, ghidul despre protecție și configurarea paginii arată cum se așază aceste setări lângă zonele de tipărire și antete, iar validarea datelor, AutoFilter și tabele se potrivește firesc cu lăsarea xlsxSpoAutoFilter deschis pe un raport blocat

Modelul de protecție detaliat și restul motorului de citire/scriere XLSX sunt incluse în HotXLS Component pentru Delphi și C++Builder; pagina produsului include întreaga API pentru foi de lucru, inclusiv referința completă pentru opțiunile de protecție