debian-mirror-gitlab/app/models/wiki_page.rb

408 lines
11 KiB
Ruby
Raw Normal View History

2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
# rubocop:disable Rails/ActiveRecordAliases
2014-09-02 18:07:02 +05:30
class WikiPage
2020-04-22 19:07:51 +05:30
include Gitlab::Utils::StrongMemoize
2017-09-10 17:25:29 +05:30
PageChangedError = Class.new(StandardError)
2018-03-17 18:26:18 +05:30
PageRenameError = Class.new(StandardError)
2020-04-22 19:07:51 +05:30
FrontMatterTooLong = Class.new(StandardError)
2020-03-13 15:44:24 +05:30
2014-09-02 18:07:02 +05:30
include ActiveModel::Validations
include ActiveModel::Conversion
include StaticModel
extend ActiveModel::Naming
2020-04-22 19:07:51 +05:30
delegate :content, :front_matter, to: :parsed_content
2014-09-02 18:07:02 +05:30
def self.primary_key
'slug'
end
def self.model_name
ActiveModel::Name.new(self, nil, 'wiki')
end
2020-04-22 19:07:51 +05:30
def eql?(other)
return false unless other.present? && other.is_a?(self.class)
2020-05-24 23:13:21 +05:30
slug == other.slug && wiki.container == other.wiki.container
2020-04-22 19:07:51 +05:30
end
alias_method :==, :eql?
2017-08-17 22:00:37 +05:30
def self.unhyphenize(name)
name.gsub(/-+/, ' ')
end
2014-09-02 18:07:02 +05:30
def to_key
[:slug]
end
validates :title, presence: true
validates :content, presence: true
2020-03-13 15:44:24 +05:30
validate :validate_path_limits, if: :title_changed?
2020-10-24 23:57:45 +05:30
validate :validate_content_size_limit, if: :content_changed?
2014-09-02 18:07:02 +05:30
2020-05-24 23:13:21 +05:30
# The GitLab Wiki instance.
2014-09-02 18:07:02 +05:30
attr_reader :wiki
2020-05-24 23:13:21 +05:30
delegate :container, to: :wiki
2014-09-02 18:07:02 +05:30
2018-03-17 18:26:18 +05:30
# The raw Gitlab::Git::WikiPage instance.
2014-09-02 18:07:02 +05:30
attr_reader :page
# The attributes Hash used for storing and validating
2018-12-05 23:21:45 +05:30
# new Page values before writing to the raw repository.
2014-09-02 18:07:02 +05:30
attr_accessor :attributes
2016-06-02 11:05:42 +05:30
def hook_attrs
2018-11-18 11:00:15 +05:30
Gitlab::HookData::WikiPageBuilder.new(self).build
2016-06-02 11:05:42 +05:30
end
2020-04-22 19:07:51 +05:30
# Construct a new WikiPage
#
2020-05-24 23:13:21 +05:30
# @param [Wiki] wiki
2020-04-22 19:07:51 +05:30
# @param [Gitlab::Git::WikiPage] page
2020-04-08 14:13:33 +05:30
def initialize(wiki, page = nil)
2014-09-02 18:07:02 +05:30
@wiki = wiki
@page = page
@attributes = {}.with_indifferent_access
set_attributes if persisted?
end
# The escaped URL path of this page.
def slug
2020-05-24 23:13:21 +05:30
attributes[:slug].presence || wiki.wiki.preview_slug(title, format)
2014-09-02 18:07:02 +05:30
end
2020-10-24 23:57:45 +05:30
alias_method :id, :slug # required to use build_stubbed
2014-09-02 18:07:02 +05:30
2015-04-26 12:48:37 +05:30
alias_method :to_param, :slug
2014-09-02 18:07:02 +05:30
2019-02-15 15:39:39 +05:30
def human_title
2020-05-24 23:13:21 +05:30
return 'Home' if title == Wiki::HOMEPAGE
2019-02-15 15:39:39 +05:30
title
end
2014-09-02 18:07:02 +05:30
# The formatted title of this page.
def title
2020-05-24 23:13:21 +05:30
attributes[:title] || ''
2014-09-02 18:07:02 +05:30
end
# Sets the title of this page.
def title=(new_title)
2020-05-24 23:13:21 +05:30
attributes[:title] = new_title
2014-09-02 18:07:02 +05:30
end
2020-04-22 19:07:51 +05:30
def raw_content
2020-05-24 23:13:21 +05:30
attributes[:content] ||= page&.text_data
2017-08-17 22:00:37 +05:30
end
# The hierarchy of the directory this page is contained in.
def directory
2018-03-17 18:26:18 +05:30
wiki.page_title_and_dir(slug)&.last.to_s
2014-09-02 18:07:02 +05:30
end
# The markup format for the page.
def format
2020-05-24 23:13:21 +05:30
attributes[:format] || :markdown
2014-09-02 18:07:02 +05:30
end
# The commit message for this page version.
def message
version.try(:message)
end
2018-12-05 23:21:45 +05:30
# The GitLab Commit instance for this page.
2014-09-02 18:07:02 +05:30
def version
2019-07-07 11:18:12 +05:30
return unless persisted?
2014-09-02 18:07:02 +05:30
2015-04-26 12:48:37 +05:30
@version ||= @page.version
2014-09-02 18:07:02 +05:30
end
2019-10-31 01:37:42 +05:30
def path
return unless persisted?
@path ||= @page.path
end
2021-06-08 01:23:25 +05:30
# Returns a CommitCollection
#
# Queries the commits for current page's path, equivalent to
# `git log path/to/page`. Filters and options supported:
# https://gitlab.com/gitlab-org/gitaly/-/blob/master/proto/commit.proto#L322-344
2018-03-17 18:26:18 +05:30
def versions(options = {})
2014-09-02 18:07:02 +05:30
return [] unless persisted?
2021-06-08 01:23:25 +05:30
default_per_page = Kaminari.config.default_per_page
offset = [options[:page].to_i - 1, 0].max * options.fetch(:per_page, default_per_page)
wiki.repository.commits('HEAD',
path: page.path,
limit: options.fetch(:limit, default_per_page),
offset: offset)
2014-09-02 18:07:02 +05:30
end
2018-03-17 18:26:18 +05:30
def count_versions
return [] unless persisted?
2020-05-24 23:13:21 +05:30
wiki.wiki.count_page_versions(page.path)
2018-03-17 18:26:18 +05:30
end
def last_version
@last_version ||= versions(limit: 1).first
2014-09-02 18:07:02 +05:30
end
2017-09-10 17:25:29 +05:30
def last_commit_sha
2018-03-17 18:26:18 +05:30
last_version&.sha
2017-09-10 17:25:29 +05:30
end
2014-09-02 18:07:02 +05:30
# Returns boolean True or False if this instance
# is an old version of the page.
def historical?
2018-12-13 13:39:08 +05:30
return false unless last_commit_sha && version
2020-05-24 23:13:21 +05:30
page.historical? && last_commit_sha != version.sha
2014-09-02 18:07:02 +05:30
end
# Returns boolean True or False if this instance
2017-08-17 22:00:37 +05:30
# is the latest commit version of the page.
def latest?
!historical?
end
# Returns boolean True or False if this instance
# has been fully created on disk or not.
2014-09-02 18:07:02 +05:30
def persisted?
2020-05-24 23:13:21 +05:30
page.present?
2014-09-02 18:07:02 +05:30
end
# Creates a new Wiki Page.
#
# attr - Hash of attributes to set on the new page.
2018-03-17 18:26:18 +05:30
# :title - The title (optionally including dir) for the new page.
2014-09-02 18:07:02 +05:30
# :content - The raw markup content.
# :format - Optional symbol representing the
# content format. Can be any type
2020-05-24 23:13:21 +05:30
# listed in the Wiki::MARKUPS
2014-09-02 18:07:02 +05:30
# Hash.
# :message - Optional commit message to set on
# the new page.
#
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
2017-09-10 17:25:29 +05:30
def create(attrs = {})
2018-03-17 18:26:18 +05:30
update_attributes(attrs)
2014-09-02 18:07:02 +05:30
2020-04-08 14:13:33 +05:30
save do
2018-12-13 13:39:08 +05:30
wiki.create_page(title, content, format, attrs[:message])
2017-09-10 17:25:29 +05:30
end
2014-09-02 18:07:02 +05:30
end
# Updates an existing Wiki Page, creating a new version.
#
2017-09-10 17:25:29 +05:30
# attrs - Hash of attributes to be updated on the page.
# :content - The raw markup content to replace the existing.
# :format - Optional symbol representing the content format.
2020-05-24 23:13:21 +05:30
# See Wiki::MARKUPS Hash for available formats.
2017-09-10 17:25:29 +05:30
# :message - Optional commit message to set on the new version.
# :last_commit_sha - Optional last commit sha to validate the page unchanged.
2018-03-17 18:26:18 +05:30
# :title - The Title (optionally including dir) to replace existing title
2014-09-02 18:07:02 +05:30
#
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
2017-09-10 17:25:29 +05:30
def update(attrs = {})
last_commit_sha = attrs.delete(:last_commit_sha)
2018-03-17 18:26:18 +05:30
2017-09-10 17:25:29 +05:30
if last_commit_sha && last_commit_sha != self.last_commit_sha
2021-04-17 20:07:23 +05:30
raise PageChangedError, s_(
'WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{wikiLinkStart}the page%{wikiLinkEnd} and make sure your changes will not unintentionally remove theirs.')
2017-09-10 17:25:29 +05:30
end
2014-09-02 18:07:02 +05:30
2018-03-17 18:26:18 +05:30
update_attributes(attrs)
2020-04-08 14:13:33 +05:30
if title.present? && title_changed? && wiki.find_page(title).present?
2020-05-24 23:13:21 +05:30
attributes[:title] = page.title
2021-04-17 20:07:23 +05:30
raise PageRenameError, s_('WikiEdit|There is already a page with the same title in that path.')
2018-03-17 18:26:18 +05:30
end
2017-09-10 17:25:29 +05:30
2020-04-08 14:13:33 +05:30
save do
2017-09-10 17:25:29 +05:30
wiki.update_page(
2020-05-24 23:13:21 +05:30
page,
2020-04-22 19:07:51 +05:30
content: raw_content,
2017-09-10 17:25:29 +05:30
format: format,
message: attrs[:message],
title: title
)
end
2014-09-02 18:07:02 +05:30
end
# Destroys the Wiki Page.
#
# Returns boolean True or False.
def delete
2020-05-24 23:13:21 +05:30
if wiki.delete_page(page)
2014-09-02 18:07:02 +05:30
true
else
false
end
end
2017-08-17 22:00:37 +05:30
# Relative path to the partial to be used when rendering collections
# of this object.
def to_partial_path
2020-06-23 00:09:42 +05:30
'../shared/wikis/wiki_page'
2017-08-17 22:00:37 +05:30
end
2020-10-24 23:57:45 +05:30
def sha
page.version&.sha
2017-08-17 22:00:37 +05:30
end
2018-03-17 18:26:18 +05:30
def title_changed?
2020-04-08 14:13:33 +05:30
if persisted?
2020-06-23 00:09:42 +05:30
# A page's `title` will be returned from Gollum/Gitaly with any +<>
# characters changed to -, whereas the `path` preserves these characters.
path_without_extension = Pathname(page.path).sub_ext('').to_s
old_title, old_dir = wiki.page_title_and_dir(self.class.unhyphenize(path_without_extension))
2020-04-08 14:13:33 +05:30
new_title, new_dir = wiki.page_title_and_dir(self.class.unhyphenize(title))
new_title != old_title || (title.include?('/') && new_dir != old_dir)
else
title.present?
end
2018-03-17 18:26:18 +05:30
end
2020-10-24 23:57:45 +05:30
def content_changed?
if persisted?
# gollum-lib always converts CRLFs to LFs in Gollum::Wiki#normalize,
# so we need to do the same here.
# Also see https://gitlab.com/gitlab-org/gitlab/-/issues/21431
raw_content.delete("\r") != page&.text_data
else
raw_content.present?
end
end
2018-10-15 14:42:47 +05:30
# Updates the current @attributes hash by merging a hash of params
def update_attributes(attrs)
attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
2020-04-22 19:07:51 +05:30
update_front_matter(attrs)
2018-10-15 14:42:47 +05:30
attrs.slice!(:content, :format, :message, :title)
2020-04-22 19:07:51 +05:30
clear_memoization(:parsed_content) if attrs.has_key?(:content)
2018-10-15 14:42:47 +05:30
2020-05-24 23:13:21 +05:30
attributes.merge!(attrs)
2018-10-15 14:42:47 +05:30
end
2020-01-01 13:55:28 +05:30
def to_ability_name
'wiki_page'
end
2020-05-24 23:13:21 +05:30
def version_commit_timestamp
version&.commit&.committed_date
end
2020-07-28 23:09:34 +05:30
def diffs(diff_options = {})
Gitlab::Diff::FileCollection::WikiPage.new(self, diff_options: diff_options)
end
2014-09-02 18:07:02 +05:30
private
2020-04-22 19:07:51 +05:30
def serialize_front_matter(hash)
return '' unless hash.present?
YAML.dump(hash.transform_keys(&:to_s)) + "---\n"
end
def update_front_matter(attrs)
2020-05-24 23:13:21 +05:30
return unless Gitlab::WikiPages::FrontMatterParser.enabled?(container)
2020-04-22 19:07:51 +05:30
return unless attrs.has_key?(:front_matter)
fm_yaml = serialize_front_matter(attrs[:front_matter])
raise FrontMatterTooLong if fm_yaml.size > Gitlab::WikiPages::FrontMatterParser::MAX_FRONT_MATTER_LENGTH
attrs[:content] = fm_yaml + (attrs[:content].presence || content)
end
def parsed_content
strong_memoize(:parsed_content) do
2020-05-24 23:13:21 +05:30
Gitlab::WikiPages::FrontMatterParser.new(raw_content, container).parse
2020-04-22 19:07:51 +05:30
end
end
2018-03-17 18:26:18 +05:30
# Process and format the title based on the user input.
def process_title(title)
return if title.blank?
title = deep_title_squish(title)
current_dirname = File.dirname(title)
2020-05-24 23:13:21 +05:30
if persisted?
2018-03-17 18:26:18 +05:30
return title[1..-1] if current_dirname == '/'
return File.join([directory.presence, title].compact) if current_dirname == '.'
end
title
end
# This method squishes all the filename
# i.e: ' foo / bar / page_name' => 'foo/bar/page_name'
def deep_title_squish(title)
components = title.split(File::SEPARATOR).map(&:squish)
File.join(components)
end
2014-09-02 18:07:02 +05:30
def set_attributes
attributes[:slug] = @page.url_path
2014-09-02 18:07:02 +05:30
attributes[:title] = @page.title
attributes[:format] = @page.format
end
2020-04-08 14:13:33 +05:30
def save
return false unless valid?
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
unless yield
errors.add(:base, wiki.error_message)
return false
end
2014-09-02 18:07:02 +05:30
2020-04-08 14:13:33 +05:30
@page = wiki.find_page(title).page
2017-09-10 17:25:29 +05:30
set_attributes
2020-04-08 14:13:33 +05:30
true
2014-09-02 18:07:02 +05:30
end
2020-03-13 15:44:24 +05:30
def validate_path_limits
2020-05-24 23:13:21 +05:30
return unless title.present?
*dirnames, filename = title.split('/')
2020-03-13 15:44:24 +05:30
2020-05-24 23:13:21 +05:30
if filename && filename.bytesize > Gitlab::WikiPages::MAX_TITLE_BYTES
2020-04-22 19:07:51 +05:30
errors.add(:title, _("exceeds the limit of %{bytes} bytes") % {
bytes: Gitlab::WikiPages::MAX_TITLE_BYTES
})
2020-03-13 15:44:24 +05:30
end
2020-04-22 19:07:51 +05:30
invalid_dirnames = dirnames.select { |d| d.bytesize > Gitlab::WikiPages::MAX_DIRECTORY_BYTES }
2020-04-08 14:13:33 +05:30
invalid_dirnames.each do |dirname|
errors.add(:title, _('exceeds the limit of %{bytes} bytes for directory name "%{dirname}"') % {
2020-04-22 19:07:51 +05:30
bytes: Gitlab::WikiPages::MAX_DIRECTORY_BYTES,
2020-04-08 14:13:33 +05:30
dirname: dirname
})
2020-03-13 15:44:24 +05:30
end
end
2020-10-24 23:57:45 +05:30
def validate_content_size_limit
current_value = raw_content.to_s.bytesize
max_size = Gitlab::CurrentSettings.wiki_page_max_content_bytes
return if current_value <= max_size
errors.add(:content, _('is too long (%{current_value}). The maximum size is %{max_size}.') % {
current_value: ActiveSupport::NumberHelper.number_to_human_size(current_value),
max_size: ActiveSupport::NumberHelper.number_to_human_size(max_size)
})
end
2014-09-02 18:07:02 +05:30
end