บทความเทคนิค

XLSX Sheet Protection in Delphi: 15 Allow Options

คุณส่งเวิร์กบุ๊กที่เสร็จแล้วให้เพื่อนร่วมงานแล้วขอให้เขาใช้ตัวกรอง ไม่ใช่เขียนทับสูตร ดังนั้นจึงต้องป้องกันชีต ใน HotXLS รุ่นเก่ากว่า การสั่งแบบนี้จะเขียนค่าเดียวลงไฟล์ทุกครั้ง คือ <sheetProtection sheet="1" objects="1" scenarios="1"/> แบบตายตัว ชีตถูกล็อก แฮชรหัสผ่านถูกแนบมา และผู้ใช้ทำอะไรไม่ได้เลย แม้แต่การเรียงลำดับและกรองที่คุณตั้งใจเปิดไว้จริง ๆ กล่องโต้ตอบ "Protect Sheet" ของ Excel เองมีช่องทำเครื่องหมาย 15 ช่องด้วยเหตุผลนี้ และเอ็นจินเดิมก็แสดงผลได้ไม่สักช่อง ช่องว่างนี้เองที่โมเดลการป้องกันใน v2.91.0 เข้ามาอุด

HotXLS คือคอมโพเนนต์สเปรดชีต VCL แบบ native สำหรับ Delphi และ C++Builder ที่อ่านและเขียน XLS กับ XLSX ได้โดยไม่ต้องติดตั้ง Excel บทความนี้ว่าด้วยฝั่ง XLSX ของการป้องกันเวิร์กชีต ได้แก่ enum TXLSXSheetProtectionOption ใหม่, property AllowOption ที่สลับสิทธิ์แต่ละรายการ, และกฎการเข้ารหัส OOXML ข้อเดียวที่มักทำให้คนที่เขียน element <sheetProtection> ด้วยมือพลาด

สิ่งที่การป้องกันเวิร์กชีตคุ้มครองจริง ๆ

เริ่มจากขอบเขตของมันก่อน เพราะตรงนี้เป็นตัวกำหนดว่าคุณควรเชื่อถืออะไรได้แค่ไหน การป้องกันเวิร์กชีตในรูปแบบสเปรดชีต OOXML (ECMA-376) เป็นนโยบายการโต้ตอบ ไม่ใช่การเข้ารหัส มันบอกแอปพลิเคชันที่ทำตามสเปกว่าจะปฏิเสธการแก้ไขใดขณะชีตถูกป้องกัน ค่าของเซลล์ยังคงอยู่ใน xl/worksheets/sheetN.xml เป็นข้อความธรรมดา ให้แตก .xlsx ออกมาก็เห็นอยู่ตรงนั้น รหัสผ่านที่เป็นตัวเลือกจะถูกเก็บเป็นแฮชแบบเดิมขนาดสั้น ไม่ใช่กุญแจสำหรับเข้ารหัสอะไรทั้งนั้น ใครก็ตามที่เปลี่ยนชื่อไฟล์ เปิดพาร์ตนั้น แล้วลบบรรทัด <sheetProtection> ออก ก็อ่านและแก้ไขได้หมด

ดังนั้นการป้องกันจึงตอบคำถามว่า "หยุดเพื่อนร่วมงานไม่ให้เผลอทับสูตร" ไม่ใช่ "เก็บข้อมูลนี้ให้เป็นความลับจากคนที่ตั้งใจ" นั่นคือคนละปัญหาและใช้เครื่องมือคนละแบบ ถ้าต้องการความลับ คุณต้องใช้การเข้ารหัสระดับเวิร์กบุ๊กที่อธิบายไว้ใน เอาต์พุต XLSX ที่ป้องกันด้วย AES ซึ่งเข้ารหัสแพ็กเกจจริง ๆ การป้องกันชีตกับการเข้ารหัสเวิร์กบุ๊กใช้ร่วมกันได้อย่างราบรื่น แต่มีแค่ตัวหลังเท่านั้นที่เป็นตัวล็อก เก็บเส้นแบ่งนี้ให้ชัด แล้วที่เหลือของหน้านี้ก็เป็นเรื่องของการเชื่อมต่อข้อมูลเท่านั้น

ตัวเลือกทั้ง 15 รายการและ property AllowOption

ตอนนี้แต่ละเวิร์กชีตมีชุดค่าของ TXLSXSheetProtectionOption ที่อธิบายว่าผู้ใช้ยังทำอะไรได้บ้างระหว่างที่ชีตถูกป้องกัน สมาชิกแต่ละตัวแมปตรงกับแอตทริบิวต์ OOXML และช่องทำเครื่องหมายในกล่องโต้ตอบของ Excel:

  • xlsxSpoEditObjects, xlsxSpoEditScenarios: แก้ไขวัตถุวาดภาพและสถานการณ์ what-if
  • xlsxSpoFormatCells, xlsxSpoFormatColumns, xlsxSpoFormatRows: จัดรูปแบบเซลล์ คอลัมน์ และแถวใหม่
  • xlsxSpoInsertColumns, xlsxSpoInsertRows, xlsxSpoInsertHyperlinks: แทรกคอลัมน์ แถว และลิงก์
  • xlsxSpoDeleteColumns, xlsxSpoDeleteRows: ลบคอลัมน์และแถว
  • xlsxSpoSelectLockedCells, xlsxSpoSelectUnlockedCells: เลื่อนการเลือกไปยังเซลล์ที่ล็อกหรือไม่ได้ล็อก
  • xlsxSpoSort, xlsxSpoAutoFilter, xlsxSpoPivotTables: เรียงลำดับช่วง ใช้ดรอปดาวน์ AutoFilter และทำงานกับ PivotTable

คุณอ่านและเขียนบิตรายตัวผ่าน property แบบมีดัชนี AllowOption บน TXLSXWorksheet ค่า AllowOption[Opt] = True หมายถึงอนุญาตการทำงานนั้น ส่วนการตั้งเป็น False คือห้ามมัน ชุดทั้งหมดดึงได้ในครั้งเดียวผ่าน SheetProtectionOptions ซึ่งเป็น TXLSXSheetProtectionOptions (แค่ set of ตามแบบ Pascal) ดังนั้นคุณจะบันทึก คืนค่า หรือแทนที่ทั้งชุดพร้อมกันก็ได้

ค่าเริ่มต้นมีความสำคัญและตั้งใจให้เป็นแบบนั้น: เวิร์กชีตที่สร้างใหม่จะเริ่มต้นโดยอนุญาตทุกตัวเลือก คอนสตรัคเตอร์จะใส่ SheetProtectionOptions ให้ครอบคลุมช่วงเต็ม [Low(TXLSXSheetProtectionOption)..High(TXLSXSheetProtectionOption)] จากนั้นคุณค่อยลดขอบเขตลงโดยตัดการทำงานที่ไม่ต้องการออก แทนที่จะสร้างชุดสิทธิ์จากศูนย์ การเลือกแบบนี้เองที่ทำให้กฎการเข้ารหัสของ writer ด้านล่างสอดคล้องกับพฤติกรรมของ Excel

ป้องกันชีต แต่ยังเปิด sort และ filter ไว้

นี่คือกรณีใช้บ่อยแบบครบจบ: ป้องกันรายงานที่เสร็จแล้วไม่ให้โครงสร้างถูกปรับใหม่ แต่ยังให้ผู้อ่านเรียงลำดับและกรองข้อมูลได้ สังเกตว่า Protect กับตัวเลือกแยกจากกัน Protect จะเปลี่ยนชีตให้เป็นสถานะป้องกันและเก็บแฮชรหัสผ่านที่เลือกไว้ มันไม่แตะชุดตัวเลือก คุณปรับ 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 นี้บังเอิญเป็นแบบนั้น แต่เป็นเพราะ wire format ของ OOXML แสดงผ่านออกมา ซึ่งเป็นหัวข้อถัดไป

กฎการเข้ารหัส: omit แปลว่า allow, attr=0 แปลว่า forbid

นี่คือข้อเท็จจริงเพียงข้อเดียวที่ขัดความรู้สึกที่สุดในฟีเจอร์นี้ และเป็นจุดที่ <sheetProtection> ที่เขียนมือมักพลาด ใน OOXML แอตทริบิวต์รายการต่อการทำงานแต่ละตัวเป็นแฟล็ก forbid และการไม่มีมันคือสิทธิ์ ถ้าแอตทริบิวต์หายไป แปลว่าการทำงานนั้นอนุญาต ถ้าเขียนเป็น "0" แปลว่าการทำงานนั้นถูกห้ามขณะชีตถูกป้องกัน ไม่มี formatCells="1" ในไฟล์ที่ถูกต้องเพื่อสื่อว่า "อนุญาตให้จัดรูปแบบ" คุณแค่ไม่ต้องใส่แอตทริบิวต์นั้นลงไปเท่านั้นเอง (ค่าเริ่มต้นของแอตทริบิวต์ที่ไม่มีอยู่คือค่า boolean เริ่มต้นของ OOXML ซึ่งเป็น true และแอตทริบิวต์พวกนี้ถูกตั้งชื่อให้ "true" หมายถึงการแก้ไขนั้นได้รับอนุญาต)

writer ของ HotXLS สะท้อนพฤติกรรมนี้ตรงตัว มันเขียน sheet="1" เพื่อเปิดการป้องกัน แล้วไล่ชุดตัวเลือกและเขียน attr="0" เฉพาะตัวเลือกที่คุณตั้งเป็น False เท่านั้น การทำงานที่อนุญาตจะไม่ทิ้งอะไรลงไปในผลลัพธ์ ดังนั้นเวิร์กบุ๊กจากส่วนก่อนจึงถูกซีเรียลไลซ์ออกมาเป็นประมาณนี้ โดยมีแค่รายการที่ห้ามกับแฮชรหัสผ่าน:

// 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 รุ่นเก่าแล้วคาดว่าจะเห็นทุกแอตทริบิวต์ถูกเขียนออกมาครบ นี่อาจดูโล่งจนเหมือนผิด แต่จริงแล้วถูกต้อง ไฟล์ที่ระบุ sort="1" และ autoFilter="1" จะมีความหมายเดียวกันกับผู้อ่านที่ทำตามสเปก แต่ Excel เองเขียนในรูปแบบ forbids-only ที่สั้นที่สุด และการทำให้ตรงกันช่วยให้ diff เล็กและการ round-trip ไม่น่ารำคาญ แอตทริบิวต์ objects และ scenarios ใช้กฎเดียวกันทุกประการ: เป็นค่าเริ่มต้นที่อนุญาตอยู่แล้ว ดังนั้นมันจะโผล่มาเป็น "0" ก็ต่อเมื่อคุณสั่งห้าม ซึ่งกลับด้านกับสตริงเก่า objects="1" scenarios="1" ที่ถูกเขียนออกมาตายตัวเสมอ

การอ่านการป้องกันกลับมา: ความครบถ้วนของ round-trip

โมเดลสิทธิ์ที่เขียนได้แต่ไฟล์อ่านกลับไม่ได้คือทางเดินทางเดียว และอาการที่พบบ่อยคือรอบโหลด-แก้ไข-บันทึกที่ทำให้สิทธิ์เปิดกว้างขึ้นแบบเงียบ ๆ HotXLS ปิดช่องนั้นไว้ เมื่อ ParseWorksheetXml พบ element <sheetProtection> มันจะตั้งค่าสถานะชีตเป็น protected เก็บแฮชรหัสผ่านถ้ามี และถอดรหัสแอตทริบิวต์รายรายการต่อการทำงานกลับเข้า 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;

โหลดไฟล์ที่ writer สร้างขึ้นมาแล้วคุณจะได้ Sort และ AutoFilter กลับมาเป็น True ส่วน FormatCells เป็น False ตรงกับชุดที่คุณบันทึกไว้ทุกประการ ความสมมาตรนี่แหละคือจุดสำคัญ แก้เพียงเซลล์เดียวในชีตที่ถูกป้องกันแบบเปิดบางสิทธิ์ แล้วบันทึกใหม่ สิทธิ์อีกสิบสี่รายการที่ไม่ได้แตะจะยังอยู่ ไม่ได้ยุบกลับไปเป็นค่าเริ่มต้นแบบได้อย่างเดียวหรือไม่ได้อย่างเดียวเหมือนเดิม

ข้อควรรู้และข้อจำกัด

มีหลายเรื่องที่ควรรู้ก่อนจะต่อเข้ากับ pipeline รายงาน:

  • รหัสผ่านอ่อนโดยตั้งใจ การป้องกันเวิร์กชีตของ XLSX เก็บแฮช legacy ขนาด 16 บิต ซึ่งเป็นตัวเดียวกับที่ Excel ใช้มาหลายสิบปี เก็บไว้เพื่อให้ทำงานร่วมกันได้ มันช่วยกันการแก้ไขโดยไม่ตั้งใจ แต่ไม่ได้ต้านผู้โจมตี อย่ามองว่ามันเป็นตัวเก็บความลับ ถ้าต้องการการป้องกันจริง ให้เข้ารหัสเวิร์กบุ๊ก
  • ตั้งค่าตัวเลือกก่อนป้องกันได้ คุณกำหนด AllowOption ได้ไม่ว่าชีตจะถูกป้องกันอยู่หรือไม่ สวิตช์ต่าง ๆ แค่บอกว่าการป้องกันจะอนุญาตอะไรเมื่อ Protect มีผล UnProtect จะล้างสถานะป้องกันกับแฮช แต่คงชุดตัวเลือกของคุณไว้ใช้ครั้งถัดไป
  • กฎของเซลล์ที่ล็อกยังมีผล การป้องกันจะบล็อกเฉพาะการแก้ไขเซลล์ที่ตั้งแอตทริบิวต์ Locked ไว้เท่านั้น ซึ่งเป็นค่าเริ่มต้นของเวิร์กบุ๊ก การปล่อยให้บางช่วงป้อนข้อมูลได้เป็นงานของสไตล์เซลล์ ไม่ใช่ตัวเลือกการป้องกัน สองชั้นนี้ทำงานร่วมกันแบบเดียวกับใน Excel
  • นี่คือเอนจิน XLSX โมเดลตัวเลือกนี้สะท้อน property Allow* แบบเก่าของเอนจิน XLS แต่ชื่อ enum และ property ที่นี่ (xlsxSpo*, AllowOption) เป็นของ TXLSXWorksheet ใน lxHandleX ถ้าคุณควบคุมเลย์เอาต์การพิมพ์บนชีตเดียวกันด้วย บทเดินผ่านเรื่อง การป้องกันและการตั้งค่าหน้ากระดาษ จะอธิบายว่าค่าพวกนี้อยู่ร่วมกับพื้นที่พิมพ์และหัวกระดาษอย่างไร และ การตรวจสอบข้อมูล AutoFilter และตาราง ก็เข้าคู่กันตามธรรมชาติกับการเปิด xlsxSpoAutoFilter ไว้บนรายงานที่ถูกล็อก

โมเดลการป้องกันแบบละเอียดและส่วนที่เหลือของเอนจินอ่าน/เขียน XLSX มาพร้อมกับ HotXLS Component สำหรับ Delphi และ C++Builder โดยหน้าผลิตภัณฑ์มี API ของเวิร์กชีตครบชุด รวมถึงเอกสารอ้างอิงตัวเลือกการป้องกันทั้งหมด