From a5521bad4012a9eea21cb067271c8ec1810c6d8c Mon Sep 17 00:00:00 2001
From: Hiroyuki Sato <h-sato@ruby-dev.jp>
Date: Sat, 4 Mar 2017 23:03:14 +0900
Subject: [PATCH] Prevent concurrent editing wiki

---
 app/controllers/projects/wikis_controller.rb    |  5 ++++-
 app/models/wiki_page.rb                         | 17 ++++++++++++-----
 app/services/wiki_pages/update_service.rb       |  2 +-
 app/views/projects/wikis/_form.html.haml        |  2 ++
 app/views/projects/wikis/edit.html.haml         |  6 ++++++
 .../1827-prevent-concurrent-editing-wiki.yml    |  4 ++++
 .../wiki/user_updates_wiki_page_spec.rb         | 12 +++++++++++-
 spec/models/wiki_page_spec.rb                   | 14 ++++++++++++++
 8 files changed, 54 insertions(+), 8 deletions(-)
 create mode 100644 changelogs/unreleased/1827-prevent-concurrent-editing-wiki.yml

diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 2d8064c987..d27af79e00 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -56,6 +56,9 @@ class Projects::WikisController < Projects::ApplicationController
     else
       render 'edit'
     end
+  rescue WikiPage::PageChangedError
+    @conflict = true
+    render 'edit'
   end
 
   def create
@@ -125,6 +128,6 @@ class Projects::WikisController < Projects::ApplicationController
   end
 
   def wiki_params
-    params[:wiki].slice(:title, :content, :format, :message)
+    params[:wiki].slice(:title, :content, :format, :message, :last_commit_sha)
   end
 end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 2caebb496d..4c5df6937f 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -1,4 +1,6 @@
 class WikiPage
+  PageChangedError = Class.new(StandardError)
+
   include ActiveModel::Validations
   include ActiveModel::Conversion
   include StaticModel
@@ -176,17 +178,22 @@ class WikiPage
 
   # Updates an existing Wiki Page, creating a new version.
   #
-  # new_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.
+  # new_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.
   #
   # Returns the String SHA1 of the newly created page
   # or False if the save was unsuccessful.
-  def update(new_content = "", format = :markdown, message = nil)
+  def update(new_content = "", format = :markdown, message = nil, last_commit_sha = nil)
     @attributes[:content] = new_content
     @attributes[:format] = format
 
+    if last_commit_sha && last_commit_sha != commit.sha
+      raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.")
+    end
+
     save :update_page, @page, content, format, message
   end
 
diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb
index 8f6a50da83..1046bb3be0 100644
--- a/app/services/wiki_pages/update_service.rb
+++ b/app/services/wiki_pages/update_service.rb
@@ -1,7 +1,7 @@
 module WikiPages
   class UpdateService < WikiPages::BaseService
     def execute(page)
-      if page.update(@params[:content], @params[:format], @params[:message])
+      if page.update(@params[:content], @params[:format], @params[:message], @params[:last_commit_sha])
         execute_hooks(page, 'update')
       end
 
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index c52527332b..9313f9e9c9 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -2,6 +2,8 @@
   = form_errors(@page)
 
   = f.hidden_field :title, value: @page.title
+  - if @page.persisted?
+    = f.hidden_field :last_commit_sha, value: @page.commit.sha
   .form-group
     = f.label :format, class: 'control-label'
     .col-sm-10
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 8cf018da1b..2ec03a00db 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -2,6 +2,12 @@
 - page_title "Edit", @page.title.capitalize, "Wiki"
 
 %div{ class: container_class }
+  - if @conflict
+    .alert.alert-danger
+      Someone edited the page the same time you did. Please check out
+      = link_to "the page", namespace_project_wiki_path(@project.namespace, @project, @page), target: "_blank"
+      and make sure your changes will not unintentionally remove theirs.
+
   .wiki-page-header.has-sidebar-toggle
     %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
       = icon('angle-double-left')
diff --git a/changelogs/unreleased/1827-prevent-concurrent-editing-wiki.yml b/changelogs/unreleased/1827-prevent-concurrent-editing-wiki.yml
new file mode 100644
index 0000000000..3ef6544c01
--- /dev/null
+++ b/changelogs/unreleased/1827-prevent-concurrent-editing-wiki.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent concurrent editing wiki
+merge_request: 9707
+author: Hiroyuki Sato
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index f842d14fa9..a15bb3ad46 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -8,7 +8,7 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
     login_as(user)
 
     visit namespace_project_path(project.namespace, project)
-    WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
+    @wiki_page = WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
     click_link 'Wiki'
   end
 
@@ -25,6 +25,16 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
       expect(page).to have_content("Last edited by #{user.name}")
       expect(page).to have_content('My awesome wiki!')
     end
+
+    scenario 'page has been updated since the user opened the edit page' do
+      click_link 'Edit'
+
+      @wiki_page.update("Update")
+
+      click_button 'Save changes'
+
+      expect(page).to have_content 'Someone edited the page the same time you did.'
+    end
   end
 
   context 'in a group namespace' do
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 753dc938c5..d76d94c318 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -208,6 +208,20 @@ describe WikiPage, models: true do
         expect(@page.update("more content")).to be_truthy
       end
     end
+
+    context "with same last commit sha" do
+      it "returns true" do
+        last_commit_sha = @page.commit.sha
+        expect(@page.update("more content", :markdown, nil, last_commit_sha)).to be_truthy
+      end
+    end
+
+    context "with different last commit sha" do
+      it "raises exception" do
+        last_commit_sha = "xxx"
+        expect { @page.update("more content", :markdown, nil, last_commit_sha) }.to raise_error(WikiPage::PageChangedError)
+      end
+    end
   end
 
   describe "#destroy" do
-- 
2.30.9