summaryrefslogtreecommitdiffhomepage
path: root/lib/axlsx/workbook/worksheet/sheet_protection.rb
blob: abf5e452eabba86446bbb1f5e671240b67c0beb0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# frozen_string_literal: true

module Axlsx
  # The SheetProtection object manages worksheet protection options per sheet.
  class SheetProtection
    include Axlsx::OptionsParser
    include Axlsx::SerializedAttributes
    include Axlsx::Accessors

    # Creates a new SheetProtection instance
    # @option options [Boolean] sheet @see SheetProtection#sheet
    # @option options [Boolean] objects @see SheetProtection#objects
    # @option options [Boolean] scenarios @see SheetProtection#scenarios
    # @option options [Boolean] format_cells @see SheetProtection#objects
    # @option options [Boolean] format_columns @see SheetProtection#format_columns
    # @option options [Boolean] format_rows @see SheetProtection#format_rows
    # @option options [Boolean] insert_columns @see SheetProtection#insert_columns
    # @option options [Boolean] insert_rows @see SheetProtection#insert_rows
    # @option options [Boolean] insert_hyperlinks @see SheetProtection#insert_hyperlinks
    # @option options [Boolean] delete_columns @see SheetProtection#delete_columns
    # @option options [Boolean] delete_rows @see SheetProtection#delete_rows
    # @option options [Boolean] select_locked_cells @see SheetProtection#select_locked_cells
    # @option options [Boolean] sort @see SheetProtection#sort
    # @option options [Boolean] auto_filter @see SheetProtection#auto_filter
    # @option options [Boolean] pivot_tables @see SheetProtection#pivot_tables
    # @option options [Boolean] select_unlocked_cells @see SheetProtection#select_unlocked_cells
    # @option options [String] password. The password required for unlocking. @see SheetProtection#password=
    def initialize(options = {})
      @objects = @scenarios = @select_locked_cells = @select_unlocked_cells = false
      @sheet = @format_cells = @format_rows = @format_columns = @insert_columns = @insert_rows = @insert_hyperlinks = @delete_columns = @delete_rows = @sort = @auto_filter = @pivot_tables = true
      @password = nil
      parse_options options
    end

    boolean_attr_accessor :sheet, :objects, :scenarios, :format_cells, :format_columns, :format_rows,
                          :insert_columns, :insert_rows, :insert_hyperlinks, :delete_columns, :delete_rows,
                          :select_locked_cells, :sort, :auto_filter, :pivot_tables, :select_unlocked_cells

    serializable_attributes :sheet, :objects, :scenarios, :format_cells, :format_columns, :format_rows,
                            :insert_columns, :insert_rows, :insert_hyperlinks, :delete_columns, :delete_rows,
                            :select_locked_cells, :sort, :auto_filter, :pivot_tables, :select_unlocked_cells, :salt, :password

    # Specifies the salt which was prepended to the user-supplied password before it was hashed using the hashing algorithm
    # @return [String]
    attr_reader :salt_value

    # Password hash
    # @return [String]
    # default nil
    attr_reader :password

    # This block is intended to implement the salt_value, hash_value and spin count as per the ECMA-376 standard.
    # However, it does not seem to actually work in EXCEL - instead they are using their old retro algorithm shown below
    # defined in the transitional portion of the speck. I am leaving this code in in the hope that someday Ill be able to
    # figure out why it does not work, and if Excel even supports it.
    #     def propper_password=(v)
    #       @algorithm_name = v == nil ? nil : 'SHA-1'
    #       @salt_value = @spin_count = @hash_value = v if v == nil
    #       return if v == nil
    #       require 'digest/sha1'
    #       @spin_count = 10000
    #       @salt_value = Digest::SHA1.hexdigest(rand(36**8).to_s(36))
    #       @spin_count.times do |count|
    #         @hash_value = Digest::SHA1.hexdigest((@hash_value ||= (@salt_value + v.to_s)) + Array(count).pack('V'))
    #       end
    #     end

    # encodes password for protection locking
    def password=(v)
      return if v.nil?

      @password = create_password_hash(v)
    end

    # Serialize the object
    # @param [String] str
    # @return [String]
    def to_xml_string(str = +'')
      serialized_tag('sheetProtection', str)
    end

    private

    # Creates a password hash for a given password
    # @return [String]
    def create_password_hash(password)
      encoded_password = encode_password(password)

      password_as_hex = [encoded_password].pack("v")
      password_as_string = password_as_hex.unpack1("H*").upcase

      password_as_string[2..3] + password_as_string[0..1]
    end

    # Encodes a given password
    # Based on the algorithm provided by Daniel Rentz of OpenOffice.
    # https://www.openoffice.org/sc/excelfileformat.pdf, Revision 1.42, page 115 (21.05.2012)
    # @return [String]
    def encode_password(password)
      i = 0
      chars = password.chars
      count = chars.size

      chars.collect! do |char|
        i += 1
        char     = char.unpack1('c') << i # ord << i
        low_15   = char & 0x7fff
        high_15  = char & (0x7fff << 15)
        high_15  = high_15 >> 15
        low_15 | high_15
      end

      encoded_password = 0x0000
      chars.each { |c| encoded_password ^= c }
      encoded_password ^= count
      encoded_password ^= 0xCE4B
    end
  end
end