wiki_page.rb 7.79 KB
Newer Older
1
class WikiPage
2
  PageChangedError = Class.new(StandardError)
3
  PageRenameError = Class.new(StandardError)
4

5 6 7 8 9 10 11 12 13 14 15 16 17
  include ActiveModel::Validations
  include ActiveModel::Conversion
  include StaticModel
  extend ActiveModel::Naming

  def self.primary_key
    'slug'
  end

  def self.model_name
    ActiveModel::Name.new(self, nil, 'wiki')
  end

18 19 20 21
  # Sorts and groups pages by directory.
  #
  # pages - an array of WikiPage objects.
  #
22 23 24
  # 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.
25
  def self.group_by_directory(pages)
26 27
    return [] if pages.blank?

28 29 30
    pages.sort_by { |page| [page.directory, page.slug] }
      .group_by(&:directory)
      .map do |dir, pages|
31
        if dir.present?
32
          WikiDirectory.new(dir, pages)
33 34
        else
          pages
35
        end
36 37
      end
      .flatten
38 39
  end

40 41 42 43
  def self.unhyphenize(name)
    name.gsub(/-+/, ' ')
  end

44 45 46 47 48 49 50
  def to_key
    [:slug]
  end

  validates :title, presence: true
  validates :content, presence: true

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
51
  # The Gitlab ProjectWiki instance.
52 53
  attr_reader :wiki

54
  # The raw Gitlab::Git::WikiPage instance.
55 56 57 58 59 60
  attr_reader :page

  # The attributes Hash used for storing and validating
  # new Page values before writing to the Gollum repository.
  attr_accessor :attributes

61 62 63 64
  def hook_attrs
    attributes
  end

65 66 67 68 69 70 71 72 73 74 75
  def initialize(wiki, page = nil, persisted = false)
    @wiki       = wiki
    @page       = page
    @persisted  = persisted
    @attributes = {}.with_indifferent_access

    set_attributes if persisted?
  end

  # The escaped URL path of this page.
  def slug
76 77 78
    if @attributes[:slug].present?
      @attributes[:slug]
    else
79
      wiki.wiki.preview_slug(title, format)
80
    end
81 82
  end

83
  alias_method :to_param, :slug
84 85 86

  # The formatted title of this page.
  def title
87
    if @attributes[:title]
88
      CGI.unescape_html(self.class.unhyphenize(@attributes[:title]))
89 90 91
    else
      ""
    end
92 93 94 95 96 97 98 99 100
  end

  # Sets the title of this page.
  def title=(new_title)
    @attributes[:title] = new_title
  end

  # The raw content of this page.
  def content
101
    @attributes[:content] ||= @page&.text_data
102 103
  end

104 105
  # The hierarchy of the directory this page is contained in.
  def directory
106
    wiki.page_title_and_dir(slug)&.last.to_s
107 108
  end

109 110
  # The processed/formatted content of this page.
  def formatted_content
111 112 113 114
    # Assuming @page exists, nil formatted_data means we didn't load it
    # before hand (i.e. page was fetched by Gitaly), so we fetch it separately.
    # If the page was fetched by Gollum, formatted_data would've been a String.
    @attributes[:formatted_content] ||= @page&.formatted_data || @wiki.page_formatted_data(@page)
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
  end

  # 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

  # The Gitlab Commit instance for this page.
  def version
    return nil unless persisted?

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
131
    @version ||= @page.version
132 133
  end

134
  def versions(options = {})
135 136
    return [] unless persisted?

137
    wiki.wiki.page_versions(@page.path, options)
138 139
  end

140 141 142 143 144 145 146 147
  def count_versions
    return [] unless persisted?

    wiki.wiki.count_page_versions(@page.path)
  end

  def last_version
    @last_version ||= versions(limit: 1).first
148 149
  end

150
  def last_commit_sha
151
    last_version&.sha
152 153
  end

154 155 156 157 158 159 160 161 162
  # Returns the Date that this latest version was
  # created on.
  def created_at
    @page.version.date
  end

  # Returns boolean True or False if this instance
  # is an old version of the page.
  def historical?
163
    @page.historical? && last_version.sha != version.sha
164 165 166
  end

  # Returns boolean True or False if this instance
167 168 169 170 171
  # is the latest commit version of the page.
  def latest?
    !historical?
  end

172
  # Returns boolean True or False if this instance
Dongqing Hu's avatar
Dongqing Hu committed
173
  # has been fully created on disk or not.
174 175 176 177 178 179 180
  def persisted?
    @persisted == true
  end

  # Creates a new Wiki Page.
  #
  # attr - Hash of attributes to set on the new page.
181
  #       :title   - The title (optionally including dir) for the new page.
182 183 184
  #       :content - The raw markup content.
  #       :format  - Optional symbol representing the
  #                  content format. Can be any type
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
185
  #                  listed in the ProjectWiki::MARKUPS
186 187 188 189 190 191
  #                  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.
192
  def create(attrs = {})
193
    update_attributes(attrs)
194

195 196 197
    save(page_details: title) do
      wiki.create_page(title, content, format, message)
    end
198 199 200 201
  end

  # Updates an existing Wiki Page, creating a new version.
  #
202 203 204 205 206 207
  # 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.
  #                           See ProjectWiki::MARKUPS Hash for available formats.
  #        :message         - Optional commit message to set on the new version.
  #        :last_commit_sha - Optional last commit sha to validate the page unchanged.
208
  #        :title           - The Title (optionally including dir) to replace existing title
209 210 211
  #
  # Returns the String SHA1 of the newly created page
  # or False if the save was unsuccessful.
212 213
  def update(attrs = {})
    last_commit_sha = attrs.delete(:last_commit_sha)
214

215
    if last_commit_sha && last_commit_sha != self.last_commit_sha
216
      raise PageChangedError
217 218
    end

219 220 221 222 223 224 225
    update_attributes(attrs)

    if title_changed?
      page_details = title

      if wiki.find_page(page_details).present?
        @attributes[:title] = @page.url_path
226
        raise PageRenameError
227
      end
228 229 230
    else
      page_details = @page.url_path
    end
231 232 233 234 235 236 237 238 239 240

    save(page_details: page_details) do
      wiki.update_page(
        @page,
        content: content,
        format: format,
        message: attrs[:message],
        title: title
      )
    end
241 242
  end

Johannes Schleifenbaum's avatar
Johannes Schleifenbaum committed
243
  # Destroys the Wiki Page.
244 245 246 247 248 249 250 251 252 253
  #
  # Returns boolean True or False.
  def delete
    if wiki.delete_page(@page)
      true
    else
      false
    end
  end

254 255 256 257 258 259
  # Relative path to the partial to be used when rendering collections
  # of this object.
  def to_partial_path
    'projects/wikis/wiki_page'
  end

260 261 262 263
  def id
    page.version.to_s
  end

264 265 266 267
  def title_changed?
    title.present? && self.class.unhyphenize(@page.url_path) != title
  end

268 269
  private

270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
  # 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

  # 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?

    attrs.slice!(:content, :format, :message, :title)

    @attributes.merge!(attrs)
  end

302
  def set_attributes
303
    attributes[:slug] = @page.url_path
304 305 306 307
    attributes[:title] = @page.title
    attributes[:format] = @page.format
  end

308 309
  def save(page_details:)
    return unless valid?
Dongqing Hu's avatar
Dongqing Hu committed
310

311 312 313 314
    unless yield
      errors.add(:base, wiki.error_message)
      return false
    end
315

316
    page_title, page_dir = wiki.page_title_and_dir(page_details)
317 318
    gitlab_git_wiki = wiki.wiki
    @page = gitlab_git_wiki.page(title: page_title, dir: page_dir)
319

320 321
    set_attributes
    @persisted = errors.blank?
322 323
  end
end