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?
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
2020-02-13 07:08:49 -05:00
validate :validate_path_limits , if : :title_changed?
2020-07-17 11:09:13 -04:00
validate :validate_content_size_limit , if : :content_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
2020-04-24 11:09:37 -04:00
attributes [ :slug ] . presence || wiki . wiki . preview_slug ( title , format )
2013-03-03 22:43:52 -05:00
end
2020-07-20 08:09:34 -04:00
alias_method :id , :slug # required to use build_stubbed
2013-03-03 22:43:52 -05:00
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
2020-04-24 11:09:37 -04:00
return 'Home' if title == Wiki :: HOMEPAGE
2018-11-27 01:22:16 -05:00
title
end
2013-03-03 22:43:52 -05:00
# The formatted title of this page.
def title
2020-04-24 11:09:37 -04:00
attributes [ :title ] || ''
2013-03-03 22:43:52 -05:00
end
# Sets the title of this page.
def title = ( new_title )
2020-04-24 11:09:37 -04:00
attributes [ :title ] = new_title
2013-03-03 22:43:52 -05:00
end
2020-04-14 23:09:11 -04:00
def raw_content
2020-04-24 11:09:37 -04: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
2020-04-24 11:09:37 -04:00
attributes [ :format ] || :markdown
2013-03-03 22:43:52 -05:00
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
2021-05-14 08:10:58 -04: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 06:48:32 -05:00
def versions ( options = { } )
2013-03-03 22:43:52 -05:00
return [ ] unless persisted?
2021-05-14 08:10:58 -04: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-03 22:43:52 -05:00
end
2017-11-17 06:48:32 -05:00
def count_versions
return [ ] unless persisted?
2020-04-24 11:09:37 -04:00
wiki . wiki . count_page_versions ( page . path )
2017-11-17 06:48:32 -05:00
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
2020-04-24 11:09:37 -04:00
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-04-24 11:09:37 -04: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
2021-02-23 16:10:44 -05: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 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?
2020-04-24 11:09:37 -04:00
attributes [ :title ] = page . title
2021-02-23 16:10:44 -05:00
raise PageRenameError , s_ ( 'WikiEdit|There is already a page with the same title in that path.' )
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 (
2020-04-24 11:09:37 -04:00
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
2020-04-24 11:09:37 -04:00
if wiki . delete_page ( page )
2013-03-03 22:43:52 -05:00
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-06-11 11:08:36 -04:00
'../shared/wikis/wiki_page'
2016-12-26 20:51:34 -05:00
end
2020-07-20 08:09:34 -04:00
def sha
page . version & . sha
2016-12-30 08:26:30 -05:00
end
2018-02-05 12:17:21 -05:00
def title_changed?
2020-03-03 10:08:08 -05:00
if persisted?
2020-06-05 17:08:27 -04: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 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
2020-07-17 11:09:13 -04:00
def content_changed?
2020-07-30 14:09:39 -04: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 11:09:13 -04: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
2020-04-24 11:09:37 -04:00
attributes . merge! ( attrs )
2018-04-18 13:50:56 -04:00
end
2019-12-17 10:08:15 -05:00
def to_ability_name
'wiki_page'
end
2020-05-11 23:09:31 -04:00
def version_commit_timestamp
version & . commit & . committed_date
end
2020-07-02 11:09:08 -04:00
def diffs ( diff_options = { } )
Gitlab :: Diff :: FileCollection :: WikiPage . new ( self , diff_options : diff_options )
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 )
2020-04-24 11:09:37 -04:00
if persisted?
2018-02-05 12:17:21 -05: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-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
2020-04-24 11:09:37 -04:00
return unless title . present?
* dirnames , filename = title . split ( '/' )
2020-02-13 07:08:49 -05:00
2020-04-24 11:09:37 -04:00
if filename && filename . bytesize > Gitlab :: WikiPages :: MAX_TITLE_BYTES
2020-04-14 23:09:11 -04:00
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
2020-07-17 11:09:13 -04: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-03 22:43:52 -05:00
end