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