Welcome to mirror list, hosted at ThFree Co, Russian Federation.

front_matter_parser.rb « wiki_pages « gitlab « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: ee30fa907f4aac2409ab90d619e0461235b1a62a (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
# frozen_string_literal: true

module Gitlab
  module WikiPages
    class FrontMatterParser
      # We limit the maximum length of text we are prepared to parse as YAML, to
      # avoid exploitations and attempts to consume memory and CPU. We allow for:
      #  - a title line
      #  - a "slugs:" line
      #  - and up to 50 slugs
      #
      # This limit does not take comments into account.
      MAX_SLUGS = 50
      SLUG_LINE_LENGTH = (4 + Gitlab::WikiPages::MAX_DIRECTORY_BYTES + 1 + Gitlab::WikiPages::MAX_TITLE_BYTES)
      MAX_FRONT_MATTER_LENGTH = (8 + Gitlab::WikiPages::MAX_TITLE_BYTES) + 7 + (SLUG_LINE_LENGTH * MAX_SLUGS)

      ParseError = Class.new(StandardError)

      class Result
        attr_reader :front_matter, :content, :reason, :error

        def initialize(content:, front_matter: {}, reason: nil, error: nil)
          @content      = content
          @front_matter = front_matter.freeze
          @reason       = reason
          @error        = error
        end
      end

      # @param [String] wiki_content
      def initialize(wiki_content)
        @wiki_content = wiki_content
      end

      def parse
        return empty_result unless wiki_content.present?
        return empty_result(block.error) unless block.valid?

        Result.new(front_matter: block.data, content: strip_front_matter_block)
      rescue ParseError => error
        empty_result(:parse_error, error)
      end

      class Block
        include Gitlab::Utils::StrongMemoize

        def initialize(delim = nil, lang = '', text = nil)
          @lang = lang.downcase.presence || Gitlab::FrontMatter::DELIM_LANG[delim]
          @text = text&.strip!
        end

        def data
          @data ||= YAML.safe_load(text, symbolize_names: true)
        rescue Psych::DisallowedClass, Psych::SyntaxError => error
          raise ParseError, error.message
        end

        def valid?
          error.nil?
        end

        def error
          strong_memoize(:error) { no_match? || too_long? || not_yaml? || not_mapping? }
        end

        private

        attr_reader :lang, :text

        def no_match?
          :no_match if text.nil?
        end

        def not_yaml?
          :not_yaml if lang != 'yaml'
        end

        def too_long?
          :too_long if text.size > MAX_FRONT_MATTER_LENGTH
        end

        def not_mapping?
          :not_mapping unless data.is_a?(Hash)
        end
      end

      private

      attr_reader :wiki_content

      def empty_result(reason = nil, error = nil)
        Result.new(content: wiki_content, reason: reason, error: error)
      end

      def block
        @block ||= parse_front_matter_block
      end

      def parse_front_matter_block
        wiki_content.match(Gitlab::FrontMatter::PATTERN) { |m| Block.new(m[:delim], m[:lang], m[:front_matter]) } || Block.new
      end

      def strip_front_matter_block
        wiki_content.gsub(Gitlab::FrontMatter::PATTERN, '')
      end
    end
  end
end