2018-07-25 05:30:33 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2018-07-02 09:57:38 -04:00
|
|
|
# rubocop:disable Rails/ActiveRecordAliases
|
2013-03-03 22:43:52 -05:00
|
|
|
class WikiPage
|
2020-04-14 23:09:11 -04:00
|
|
|
include Gitlab::Utils::StrongMemoize
|
|
|
|
|
2017-03-04 09:03:14 -05:00
|
|
|
PageChangedError = Class.new(StandardError)
|
2018-02-05 12:17:21 -05:00
|
|
|
PageRenameError = Class.new(StandardError)
|
2020-04-14 23:09:11 -04:00
|
|
|
FrontMatterTooLong = Class.new(StandardError)
|
2020-02-13 07:08:49 -05:00
|
|
|
|
2013-03-03 22:43:52 -05:00
|
|
|
include ActiveModel::Validations
|
|
|
|
include ActiveModel::Conversion
|
|
|
|
include StaticModel
|
|
|
|
extend ActiveModel::Naming
|
|
|
|
|
2020-04-14 23:09:11 -04:00
|
|
|
delegate :content, :front_matter, to: :parsed_content
|
|
|
|
|
2013-03-03 22:43:52 -05:00
|
|
|
def self.primary_key
|
|
|
|
'slug'
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.model_name
|
2019-10-31 17:06:28 -04:00
|
|
|
ActiveModel::Name.new(self, nil, 'wiki')
|
|
|
|
end
|
|
|
|
|
2020-03-19 08:09:33 -04:00
|
|
|
def eql?(other)
|
|
|
|
return false unless other.present? && other.is_a?(self.class)
|
|
|
|
|
2020-04-22 08:09:29 -04:00
|
|
|
slug == other.slug && wiki.container == other.wiki.container
|
2020-03-19 08:09:33 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
alias_method :==, :eql?
|
|
|
|
|
2019-10-31 17:06:28 -04:00
|
|
|
# Sorts and groups pages by directory.
|
|
|
|
#
|
|
|
|
# pages - an array of WikiPage objects.
|
|
|
|
#
|
|
|
|
# Returns an array of WikiPage and WikiDirectory objects. The entries are
|
|
|
|
# sorted by alphabetical order (directories and pages inside each directory).
|
|
|
|
# Pages at the root level come before everything.
|
|
|
|
def self.group_by_directory(pages)
|
|
|
|
return [] if pages.blank?
|
|
|
|
|
|
|
|
pages.each_with_object([]) do |page, grouped_pages|
|
|
|
|
next grouped_pages << page unless page.directory.present?
|
|
|
|
|
|
|
|
directory = grouped_pages.find do |obj|
|
|
|
|
obj.is_a?(WikiDirectory) && obj.slug == page.directory
|
|
|
|
end
|
|
|
|
|
|
|
|
next directory.pages << page if directory
|
|
|
|
|
|
|
|
grouped_pages << WikiDirectory.new(page.directory, [page])
|
|
|
|
end
|
2016-12-15 23:12:21 -05:00
|
|
|
end
|
|
|
|
|
2017-01-29 22:07:31 -05:00
|
|
|
def self.unhyphenize(name)
|
|
|
|
name.gsub(/-+/, ' ')
|
|
|
|
end
|
|
|
|
|
2013-03-03 22:43:52 -05:00
|
|
|
def to_key
|
|
|
|
[:slug]
|
|
|
|
end
|
|
|
|
|
|
|
|
validates :title, presence: true
|
|
|
|
validates :content, presence: true
|
2020-02-13 07:08:49 -05:00
|
|
|
validate :validate_path_limits, if: :title_changed?
|
2013-03-03 22:43:52 -05:00
|
|
|
|
2020-04-22 08:09:29 -04:00
|
|
|
# The GitLab Wiki instance.
|
2013-03-03 22:43:52 -05:00
|
|
|
attr_reader :wiki
|
2020-04-22 08:09:29 -04:00
|
|
|
delegate :container, to: :wiki
|
2013-03-03 22:43:52 -05:00
|
|
|
|
2017-10-03 12:58:33 -04:00
|
|
|
# The raw Gitlab::Git::WikiPage instance.
|
2013-03-03 22:43:52 -05:00
|
|
|
attr_reader :page
|
|
|
|
|
|
|
|
# The attributes Hash used for storing and validating
|
2018-10-01 23:21:46 -04:00
|
|
|
# new Page values before writing to the raw repository.
|
2013-03-03 22:43:52 -05:00
|
|
|
attr_accessor :attributes
|
|
|
|
|
2016-02-28 02:26:52 -05:00
|
|
|
def hook_attrs
|
2018-06-20 09:53:19 -04:00
|
|
|
Gitlab::HookData::WikiPageBuilder.new(self).build
|
2016-02-28 02:26:52 -05:00
|
|
|
end
|
|
|
|
|
2020-03-19 08:09:33 -04:00
|
|
|
# Construct a new WikiPage
|
|
|
|
#
|
2020-04-22 08:09:29 -04:00
|
|
|
# @param [Wiki] wiki
|
2020-03-19 08:09:33 -04:00
|
|
|
# @param [Gitlab::Git::WikiPage] page
|
2020-03-03 10:08:08 -05:00
|
|
|
def initialize(wiki, page = nil)
|
2013-03-03 22:43:52 -05:00
|
|
|
@wiki = wiki
|
|
|
|
@page = page
|
|
|
|
@attributes = {}.with_indifferent_access
|
|
|
|
|
|
|
|
set_attributes if persisted?
|
|
|
|
end
|
|
|
|
|
|
|
|
# The escaped URL path of this page.
|
|
|
|
def slug
|
2019-09-26 05:06:04 -04:00
|
|
|
@attributes[:slug].presence || wiki.wiki.preview_slug(title, format)
|
2013-03-03 22:43:52 -05:00
|
|
|
end
|
|
|
|
|
2015-02-02 23:57:10 -05:00
|
|
|
alias_method :to_param, :slug
|
2013-03-03 22:43:52 -05:00
|
|
|
|
2018-11-27 01:22:16 -05:00
|
|
|
def human_title
|
|
|
|
return 'Home' if title == 'home'
|
|
|
|
|
|
|
|
title
|
|
|
|
end
|
|
|
|
|
2013-03-03 22:43:52 -05:00
|
|
|
# The formatted title of this page.
|
|
|
|
def title
|
2020-03-03 10:08:08 -05:00
|
|
|
@attributes[:title] || ''
|
2013-03-03 22:43:52 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
# Sets the title of this page.
|
|
|
|
def title=(new_title)
|
|
|
|
@attributes[:title] = new_title
|
|
|
|
end
|
|
|
|
|
2020-04-14 23:09:11 -04:00
|
|
|
def raw_content
|
2017-02-07 09:16:46 -05:00
|
|
|
@attributes[:content] ||= @page&.text_data
|
2013-03-03 22:43:52 -05:00
|
|
|
end
|
|
|
|
|
2016-12-18 18:22:20 -05:00
|
|
|
# The hierarchy of the directory this page is contained in.
|
|
|
|
def directory
|
2018-02-05 12:17:21 -05:00
|
|
|
wiki.page_title_and_dir(slug)&.last.to_s
|
2016-12-18 18:22:20 -05:00
|
|
|
end
|
|
|
|
|
2013-03-03 22:43:52 -05:00
|
|
|
# The markup format for the page.
|
|
|
|
def format
|
|
|
|
@attributes[:format] || :markdown
|
|
|
|
end
|
|
|
|
|
|
|
|
# The commit message for this page version.
|
|
|
|
def message
|
|
|
|
version.try(:message)
|
|
|
|
end
|
|
|
|
|
2018-09-21 08:05:37 -04:00
|
|
|
# The GitLab Commit instance for this page.
|
2013-03-03 22:43:52 -05:00
|
|
|
def version
|
2019-02-08 07:19:53 -05:00
|
|
|
return unless persisted?
|
2013-03-03 22:43:52 -05:00
|
|
|
|
2014-09-25 06:56:23 -04:00
|
|
|
@version ||= @page.version
|
2013-03-03 22:43:52 -05:00
|
|
|
end
|
|
|
|
|
2019-10-03 00:01:57 -04:00
|
|
|
def path
|
|
|
|
return unless persisted?
|
|
|
|
|
|
|
|
@path ||= @page.path
|
|
|
|
end
|
|
|
|
|
2017-11-17 06:48:32 -05:00
|
|
|
def versions(options = {})
|
2013-03-03 22:43:52 -05:00
|
|
|
return [] unless persisted?
|
|
|
|
|
2017-11-17 06:48:32 -05:00
|
|
|
wiki.wiki.page_versions(@page.path, options)
|
2013-03-03 22:43:52 -05:00
|
|
|
end
|
|
|
|
|
2017-11-17 06:48:32 -05:00
|
|
|
def count_versions
|
|
|
|
return [] unless persisted?
|
|
|
|
|
|
|
|
wiki.wiki.count_page_versions(@page.path)
|
|
|
|
end
|
|
|
|
|
|
|
|
def last_version
|
|
|
|
@last_version ||= versions(limit: 1).first
|
2013-08-16 09:59:26 -04:00
|
|
|
end
|
|
|
|
|
2017-07-27 09:00:06 -04:00
|
|
|
def last_commit_sha
|
2017-11-17 06:48:32 -05:00
|
|
|
last_version&.sha
|
2017-07-27 09:00:06 -04:00
|
|
|
end
|
|
|
|
|
2013-03-03 22:43:52 -05:00
|
|
|
# Returns boolean True or False if this instance
|
|
|
|
# is an old version of the page.
|
|
|
|
def historical?
|
2018-10-15 15:07:02 -04:00
|
|
|
return false unless last_commit_sha && version
|
|
|
|
|
|
|
|
@page.historical? && last_commit_sha != version.sha
|
2013-03-03 22:43:52 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
# Returns boolean True or False if this instance
|
2017-03-15 01:44:05 -04:00
|
|
|
# is the latest commit version of the page.
|
|
|
|
def latest?
|
|
|
|
!historical?
|
|
|
|
end
|
|
|
|
|
2013-03-03 22:43:52 -05:00
|
|
|
# Returns boolean True or False if this instance
|
2017-03-20 09:53:23 -04:00
|
|
|
# has been fully created on disk or not.
|
2013-03-03 22:43:52 -05:00
|
|
|
def persisted?
|
2020-03-03 10:08:08 -05:00
|
|
|
@page.present?
|
2013-03-03 22:43:52 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
# Creates a new Wiki Page.
|
|
|
|
#
|
|
|
|
# attr - Hash of attributes to set on the new page.
|
2018-02-05 12:17:21 -05:00
|
|
|
# :title - The title (optionally including dir) for the new page.
|
2013-03-03 22:43:52 -05:00
|
|
|
# :content - The raw markup content.
|
|
|
|
# :format - Optional symbol representing the
|
|
|
|
# content format. Can be any type
|
2020-04-22 08:09:29 -04:00
|
|
|
# listed in the Wiki::MARKUPS
|
2013-03-03 22:43:52 -05:00
|
|
|
# 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-07-23 04:19:10 -04:00
|
|
|
def create(attrs = {})
|
2018-07-02 09:57:38 -04:00
|
|
|
update_attributes(attrs)
|
2013-03-03 22:43:52 -05:00
|
|
|
|
2020-03-03 10:08:08 -05:00
|
|
|
save do
|
2018-11-07 03:01:59 -05:00
|
|
|
wiki.create_page(title, content, format, attrs[:message])
|
2017-07-23 04:19:10 -04:00
|
|
|
end
|
2013-03-03 22:43:52 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
# Updates an existing Wiki Page, creating a new version.
|
|
|
|
#
|
2017-07-23 04:19:10 -04:00
|
|
|
# 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-04-22 08:09:29 -04:00
|
|
|
# See Wiki::MARKUPS Hash for available formats.
|
2017-07-23 04:19:10 -04:00
|
|
|
# :message - Optional commit message to set on the new version.
|
|
|
|
# :last_commit_sha - Optional last commit sha to validate the page unchanged.
|
2018-02-05 12:17:21 -05:00
|
|
|
# :title - The Title (optionally including dir) to replace existing title
|
2013-03-03 22:43:52 -05:00
|
|
|
#
|
|
|
|
# Returns the String SHA1 of the newly created page
|
|
|
|
# or False if the save was unsuccessful.
|
2017-07-23 04:19:10 -04:00
|
|
|
def update(attrs = {})
|
|
|
|
last_commit_sha = attrs.delete(:last_commit_sha)
|
2018-02-05 12:17:21 -05:00
|
|
|
|
2017-07-27 09:00:06 -04:00
|
|
|
if last_commit_sha && last_commit_sha != self.last_commit_sha
|
2018-02-05 12:17:21 -05:00
|
|
|
raise PageChangedError
|
2017-03-04 09:03:14 -05:00
|
|
|
end
|
|
|
|
|
2018-07-02 09:57:38 -04:00
|
|
|
update_attributes(attrs)
|
2018-02-05 12:17:21 -05:00
|
|
|
|
2020-03-03 10:08:08 -05:00
|
|
|
if title.present? && title_changed? && wiki.find_page(title).present?
|
|
|
|
@attributes[:title] = @page.title
|
|
|
|
raise PageRenameError
|
2018-02-05 12:17:21 -05:00
|
|
|
end
|
2017-07-23 04:19:10 -04:00
|
|
|
|
2020-03-03 10:08:08 -05:00
|
|
|
save do
|
2017-07-23 04:19:10 -04:00
|
|
|
wiki.update_page(
|
|
|
|
@page,
|
2020-04-14 23:09:11 -04:00
|
|
|
content: raw_content,
|
2017-07-23 04:19:10 -04:00
|
|
|
format: format,
|
|
|
|
message: attrs[:message],
|
|
|
|
title: title
|
|
|
|
)
|
|
|
|
end
|
2013-03-03 22:43:52 -05:00
|
|
|
end
|
|
|
|
|
2013-07-29 06:46:00 -04:00
|
|
|
# Destroys the Wiki Page.
|
2013-03-03 22:43:52 -05:00
|
|
|
#
|
|
|
|
# Returns boolean True or False.
|
|
|
|
def delete
|
|
|
|
if wiki.delete_page(@page)
|
|
|
|
true
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-10-31 17:06:28 -04:00
|
|
|
# Relative path to the partial to be used when rendering collections
|
|
|
|
# of this object.
|
|
|
|
def to_partial_path
|
2020-04-22 08:09:29 -04:00
|
|
|
# TODO: Move into shared/ with https://gitlab.com/gitlab-org/gitlab/-/issues/196054
|
2019-10-31 17:06:28 -04:00
|
|
|
'projects/wikis/wiki_page'
|
2016-12-26 20:51:34 -05:00
|
|
|
end
|
|
|
|
|
2016-12-30 08:26:30 -05:00
|
|
|
def id
|
|
|
|
page.version.to_s
|
|
|
|
end
|
|
|
|
|
2018-02-05 12:17:21 -05:00
|
|
|
def title_changed?
|
2020-03-03 10:08:08 -05:00
|
|
|
if persisted?
|
|
|
|
old_title, old_dir = wiki.page_title_and_dir(self.class.unhyphenize(@page.url_path))
|
2020-03-12 17:09:45 -04:00
|
|
|
new_title, new_dir = wiki.page_title_and_dir(self.class.unhyphenize(title))
|
2020-03-03 10:08:08 -05:00
|
|
|
|
|
|
|
new_title != old_title || (title.include?('/') && new_dir != old_dir)
|
|
|
|
else
|
|
|
|
title.present?
|
|
|
|
end
|
2018-02-05 12:17:21 -05:00
|
|
|
end
|
|
|
|
|
2018-04-18 13:50:56 -04:00
|
|
|
# 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-14 23:09:11 -04:00
|
|
|
update_front_matter(attrs)
|
2018-04-18 13:50:56 -04:00
|
|
|
|
|
|
|
attrs.slice!(:content, :format, :message, :title)
|
2020-04-14 23:09:11 -04:00
|
|
|
clear_memoization(:parsed_content) if attrs.has_key?(:content)
|
2018-04-18 13:50:56 -04:00
|
|
|
|
|
|
|
@attributes.merge!(attrs)
|
|
|
|
end
|
|
|
|
|
2019-12-17 10:08:15 -05:00
|
|
|
def to_ability_name
|
|
|
|
'wiki_page'
|
|
|
|
end
|
|
|
|
|
2013-03-03 22:43:52 -05:00
|
|
|
private
|
|
|
|
|
2020-04-14 23:09:11 -04:00
|
|
|
def serialize_front_matter(hash)
|
|
|
|
return '' unless hash.present?
|
|
|
|
|
|
|
|
YAML.dump(hash.transform_keys(&:to_s)) + "---\n"
|
|
|
|
end
|
|
|
|
|
|
|
|
def update_front_matter(attrs)
|
2020-04-22 08:09:29 -04:00
|
|
|
return unless Gitlab::WikiPages::FrontMatterParser.enabled?(container)
|
2020-04-14 23:09:11 -04:00
|
|
|
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-04-22 08:09:29 -04:00
|
|
|
Gitlab::WikiPages::FrontMatterParser.new(raw_content, container).parse
|
2020-04-14 23:09:11 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-02-05 12:17:21 -05:00
|
|
|
# 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)
|
|
|
|
|
|
|
|
if @page.present?
|
|
|
|
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
|
|
|
|
|
2013-03-03 22:43:52 -05:00
|
|
|
def set_attributes
|
2016-01-08 11:23:45 -05:00
|
|
|
attributes[:slug] = @page.url_path
|
2013-03-03 22:43:52 -05:00
|
|
|
attributes[:title] = @page.title
|
|
|
|
attributes[:format] = @page.format
|
|
|
|
end
|
|
|
|
|
2020-03-03 10:08:08 -05:00
|
|
|
def save
|
|
|
|
return false unless valid?
|
2017-03-20 09:53:23 -04:00
|
|
|
|
2017-07-23 04:19:10 -04:00
|
|
|
unless yield
|
|
|
|
errors.add(:base, wiki.error_message)
|
|
|
|
return false
|
|
|
|
end
|
2013-03-03 22:43:52 -05:00
|
|
|
|
2020-03-03 10:08:08 -05:00
|
|
|
@page = wiki.find_page(title).page
|
2017-07-23 04:19:10 -04:00
|
|
|
set_attributes
|
2020-03-03 10:08:08 -05:00
|
|
|
|
|
|
|
true
|
2013-03-03 22:43:52 -05:00
|
|
|
end
|
2020-02-13 07:08:49 -05:00
|
|
|
|
|
|
|
def validate_path_limits
|
|
|
|
*dirnames, title = @attributes[:title].split('/')
|
|
|
|
|
2020-04-14 23:09:11 -04:00
|
|
|
if title && title.bytesize > Gitlab::WikiPages::MAX_TITLE_BYTES
|
|
|
|
errors.add(:title, _("exceeds the limit of %{bytes} bytes") % {
|
|
|
|
bytes: Gitlab::WikiPages::MAX_TITLE_BYTES
|
|
|
|
})
|
2020-02-13 07:08:49 -05:00
|
|
|
end
|
|
|
|
|
2020-04-14 23:09:11 -04:00
|
|
|
invalid_dirnames = dirnames.select { |d| d.bytesize > Gitlab::WikiPages::MAX_DIRECTORY_BYTES }
|
2020-03-02 13:07:42 -05:00
|
|
|
invalid_dirnames.each do |dirname|
|
|
|
|
errors.add(:title, _('exceeds the limit of %{bytes} bytes for directory name "%{dirname}"') % {
|
2020-04-14 23:09:11 -04:00
|
|
|
bytes: Gitlab::WikiPages::MAX_DIRECTORY_BYTES,
|
2020-03-02 13:07:42 -05:00
|
|
|
dirname: dirname
|
|
|
|
})
|
2020-02-13 07:08:49 -05:00
|
|
|
end
|
|
|
|
end
|
2013-03-03 22:43:52 -05:00
|
|
|
end
|