# frozen_string_literal: true module Gitlab module WikiPages class FrontMatterParser FEATURE_FLAG = :wiki_front_matter # 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 # @param [FeatureGate] feature_gate The scope for feature availability def initialize(wiki_content, feature_gate) @wiki_content = wiki_content @feature_gate = feature_gate end def self.enabled?(gate = nil) Feature.enabled?(FEATURE_FLAG, gate) end def parse return empty_result unless enabled? && 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 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, :feature_gate def empty_result(reason = nil, error = nil) Result.new(content: wiki_content, reason: reason, error: error) end def enabled? self.class.enabled?(feature_gate) end def block @block ||= parse_front_matter_block end def parse_front_matter_block wiki_content.match(Gitlab::FrontMatter::PATTERN) { |m| Block.new(*m.captures) } || Block.new end def strip_front_matter_block wiki_content.gsub(Gitlab::FrontMatter::PATTERN, '') end end end end