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
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?
2022-01-26 12:08:38 +05:30
return title [ 1 .. ] if current_dirname == '/'
2018-03-17 18:26:18 +05:30
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
2016-01-19 16:12:03 +05:30
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