From 04766697357b29521515e7c71ae5139e04dd5ba2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 4 May 2008 15:05:38 +0000 Subject: [PATCH] Wiki page protection (#851, patch #1146 by Mateo Murphy with slight changes). New permission added: protect wiki pages. Once a page is protected, it can be edited/renamed/deleted only by users who have this permission. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1415 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/wiki_controller.rb | 22 +++++++- app/models/wiki_page.rb | 5 ++ app/views/wiki/show.rhtml | 8 ++- db/migrate/093_add_wiki_pages_protected.rb | 9 ++++ lib/redmine.rb | 1 + public/images/locked.png | Bin 566 -> 1127 bytes public/images/unlock.png | Bin 643 -> 618 bytes test/fixtures/roles.yml | 2 + test/fixtures/wiki_pages.yml | 4 ++ test/functional/wiki_controller_test.rb | 56 +++++++++++++++++++++ 10 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 db/migrate/093_add_wiki_pages_protected.rb diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index 53c5ec53b..44113ebf3 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -21,7 +21,7 @@ class WikiController < ApplicationController layout 'base' before_filter :find_wiki, :authorize - verify :method => :post, :only => [:destroy, :destroy_attachment], :redirect_to => { :action => :index } + verify :method => :post, :only => [:destroy, :destroy_attachment, :protect], :redirect_to => { :action => :index } helper :attachments include AttachmentsHelper @@ -48,12 +48,14 @@ class WikiController < ApplicationController send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt") return end + @editable = editable? render :action => 'show' end # edit an existing page or a new one def edit @page = @wiki.find_or_new_page(params[:page]) + return render_403 unless editable? @page.content = WikiContent.new(:page => @page) if @page.new_record? @content = @page.content_for_version(params[:version]) @@ -82,7 +84,8 @@ class WikiController < ApplicationController # rename a page def rename - @page = @wiki.find_page(params[:page]) + @page = @wiki.find_page(params[:page]) + return render_403 unless editable? @page.redirect_existing_links = true # used to display the *original* title if some AR validation errors occur @original_title = @page.pretty_title @@ -92,6 +95,12 @@ class WikiController < ApplicationController end end + def protect + page = @wiki.find_page(params[:page]) + page.update_attribute :protected, params[:protected] + redirect_to :action => 'index', :id => @project, :page => page.title + end + # show page history def history @page = @wiki.find_page(params[:page]) @@ -122,6 +131,7 @@ class WikiController < ApplicationController # remove a wiki page and its history def destroy @page = @wiki.find_page(params[:page]) + return render_403 unless editable? @page.destroy if @page redirect_to :action => 'special', :id => @project, :page => 'Page_index' end @@ -152,6 +162,7 @@ class WikiController < ApplicationController def preview page = @wiki.find_page(params[:page]) + return render_403 unless editable?(page) @attachements = page.attachments if page @text = params[:content][:text] render :partial => 'common/preview' @@ -159,12 +170,14 @@ class WikiController < ApplicationController def add_attachment @page = @wiki.find_page(params[:page]) + return render_403 unless editable? attach_files(@page, params[:attachments]) redirect_to :action => 'index', :page => @page.title end def destroy_attachment @page = @wiki.find_page(params[:page]) + return render_403 unless editable? @page.attachments.find(params[:attachment_id]).destroy redirect_to :action => 'index', :page => @page.title end @@ -178,4 +191,9 @@ private rescue ActiveRecord::RecordNotFound render_404 end + + # Returns true if the current user is allowed to edit the page, otherwise false + def editable?(page = @page) + page.editable_by?(User.current) + end end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 8ce71cb80..95750f37b 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -105,6 +105,11 @@ class WikiPage < ActiveRecord::Base def text content.text if content end + + # Returns true if usr is allowed to edit the page, otherwise false + def editable_by?(usr) + !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project) + end end class WikiDiff diff --git a/app/views/wiki/show.rhtml b/app/views/wiki/show.rhtml index e4413d090..8092525bd 100644 --- a/app/views/wiki/show.rhtml +++ b/app/views/wiki/show.rhtml @@ -1,8 +1,12 @@
+<% if @editable %> <%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if @content.version == @page.content.version %> +<%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :page => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %> +<%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :page => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %> <%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :page => @page.title}, :class => 'icon icon-move') if @content.version == @page.content.version %> <%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :page => @page.title}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %> <%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :page => @page.title, :version => @content.version }, :class => 'icon icon-cancel') if @content.version < @page.content.version %> +<% end %> <%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
@@ -22,9 +26,9 @@ <%= render(:partial => "wiki/content", :locals => {:content => @content}) %> -<%= link_to_attachments @page.attachments, :delete_url => (authorize_for('wiki', 'destroy_attachment') ? {:controller => 'wiki', :action => 'destroy_attachment', :page => @page.title} : nil) %> +<%= link_to_attachments @page.attachments, :delete_url => ((@editable && authorize_for('wiki', 'destroy_attachment')) ? {:controller => 'wiki', :action => 'destroy_attachment', :page => @page.title} : nil) %> -<% if authorize_for('wiki', 'add_attachment') %> +<% if @editable && authorize_for('wiki', 'add_attachment') %>

<%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;", :id => 'attach_files_link' %>

<% form_tag({ :controller => 'wiki', :action => 'add_attachment', :page => @page.title }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %> diff --git a/db/migrate/093_add_wiki_pages_protected.rb b/db/migrate/093_add_wiki_pages_protected.rb new file mode 100644 index 000000000..49720fbb7 --- /dev/null +++ b/db/migrate/093_add_wiki_pages_protected.rb @@ -0,0 +1,9 @@ +class AddWikiPagesProtected < ActiveRecord::Migration + def self.up + add_column :wiki_pages, :protected, :boolean, :default => false, :null => false + end + + def self.down + remove_column :wiki_pages, :protected + end +end diff --git a/lib/redmine.rb b/lib/redmine.rb index cef2615df..b21f5716d 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -76,6 +76,7 @@ Redmine::AccessControl.map do |map| map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :special] map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment] + map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member end map.project_module :repository do |map| diff --git a/public/images/locked.png b/public/images/locked.png index c2789e35cbe70bd4d7bbf999158c5b6f4db451c2..82d629961912b414224d483158997d0318a74acb 100644 GIT binary patch literal 1127 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!UWsc&iE~kEVo7Fxofw`4|k%G2?p@G5Sw|;Fvo$Mu^zOL*ySXua_lyl#A zr2&OF3p^r=85p=efH0%e8j~47L6&q!Uq=Rpjs4tz5?L7-m>B|mLR=@+rp<26zP~;D z-j2Md2QyzB$$NRK^yTTwEnBv{xzzOWdgtfclfT|y{QchI4|jKbytnhyyA{{)5BFWWcJ0fPLtmaA`u6nDmuE-5K0o^H<*{$CPJDlT`un@H-`}18 z_3q4%cNczty8h?Or9WS;{P}d}&!>BTzdii_|3A=SqhK@y1}X&3KV>=sv|GC*$S)X} z8i@o9$|sj!0BWoBba4!+xK(=Yq)?NA0LujnmQ5Z?iH2HQo815ZXJ$McvM}_^!sWl_ zw$=P|`m=FP@$Sad^^X!Ijq?~^b4wZAV!o1VzHhF!cf>>0KCN)C7y&J{r;2?yB_B$9 zdT!Fu5|CSHxY97OsbDz+BZKYRLtn3bcfOp#Yu*1hg-JbCYEi$GeGe$yJYD@<);T3K F0RZk%`04-v delta 522 zcmV+l0`>jp2(|=}H-B?sZ7v`-AZlT5b}k??FfcbDGBG+bG&(XdAS*C2Ffh8@)=cO^mj48E@Lf+QK_vhC4tttPtIkki-ds-OptvSY&G5@VGt$Qiw*0sHiDTHGo z=dD7udny0DN^?>eLNO5UtwQIuTIa1w|GiT8#%tE4Qt!QE_q}8P#&h?^bML)t|IT~& z&U^R9d+)}3=YO?p|Nr;z#)RIjWAD9lt$QK8y}j?}*6-fE=gyqgwS>l{Yqgwf#>U3Z z&d%1>)(TKcx&QzG0d!JMQvg8b*k%9#0I5ktK~#9!P0rgEfj|@n;7;Vwq%fI;oFY+F zLOC_P|3hhN*5tp~YyIEe7n|cqB#cGwFBlg^E;U`mIDgNJTwqWZGH?U9JTHjSq-pki zy+7ZquGi!i-=idnwa2W2k$b*`VHCxtMd5hzeLn~s27C9Qd!DB-SgqC@v~_k2Ae=7{ zvTbuoVG6AYLI9Wsg)uZJ$O2EK*H?h54uGZ)>(6*Wlv-`M)2-G&N}d$~0Hf^`00000 MNkvXXt^-0~f~wp8kpKVy diff --git a/public/images/unlock.png b/public/images/unlock.png index e0d414978ad0a8d17ba340276191bce671fc43f4..f15fead727a16a9c79e86a60b37ae2dbd2b27724 100644 GIT binary patch delta 593 zcmV-X0V>IRB3Hx0Bm(`E+8>CAZ=lCb09J>FgPGF zGCD9fIxsRID=;xIFvIOmh5!Hn2XskIMF-dg1P(I~8*zDV00009a7bBm000W`000W` z0Ya=am;e9(1ZP1_K>z@;j|==^1poj64p2-~MgRZ*`PtR|;(z4%-{ADr&hXv1-?wY| z-PiGPcl2Ut_1@e2+1d0_|eVu<-+#m!u#Cd`fhUgXkhYUW%#{nU$R!;6v zRr%A`_2UByet-RQb^LE|`)hIcV`}@}-S+Cp>B@Ed>FW50kNkpq`+9%;x4QXrclvK` z_G)wZX z!otJZpaLGAUfw>wzMg*m0T2OOJ9`I5CubK|H+P7Dk+F%XnYo3fm9-5-KtoeYTSr$< z-@p)RhDww)8=JCAqjIBI|nBh8#fOxAKdYb fOhAH}g@FM8N3$56Q!blL00000NkvXXu0mjfJ!Uk9 delta 618 zcmWlVZ%7ki0LI_TiI*Ck_J^Z{HYo6ja_=x@GisZz@<^Gs54j>-r;|CR)kc3faHt~; zF8af)53WLDU!qsZxcXpM1hydR=nuYG)Z2%kxB7As#gQr3dU&1>&+pswe7B93Ufi7C zmI@q9HJ|8>q+){u_E0R*3wmOS?RHnAJrRl7T~24C9lAZP2G5Rq`&K7(I-@&1699m+ zRBKzH>}~1Bs+!6n7xT4w-Rb6#EEvdL z$U-$ESO_|JmSUCUp<7^8S+FcCN3$a2Ow07^FdKJp98e@jmMUTFmqcFG6%q4tn#OSs z5(Gt+Q}cqN@S^Sz3CPo&WtnCnV`y;$i3mBM#_ z1Hk)bAzCbIYl|;*`hA=JWc^4G*n%x^)2=m@!K&HWk|t2HZrQ546pA*Q&yR9*W50a2 zBY{0LC%dwX#?wci&V1{rzFzn4!wvm`{m9h)pMx{u!F#jI!%eq(oi*nl_jo4it{i@W zytP%akkoy5{_M4Z>FMT&K?k$yTt6D{cMS8s@n@cwU6*ZMrlHR(j@FMyQe#^t%a4nH zx$?a~YA2cKo4L=wnHsZ%XE*Fl)b^k1Zoj%%{%6JfO+M<%FB~c^0Ih-cmMMSt$Uo=z B6W;&; diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml index 1ede6fca9..e7b142268 100644 --- a/test/fixtures/roles.yml +++ b/test/fixtures/roles.yml @@ -29,6 +29,7 @@ roles_001: - :manage_documents - :view_wiki_pages - :edit_wiki_pages + - :protect_wiki_pages - :delete_wiki_pages - :rename_wiki_pages - :add_messages @@ -69,6 +70,7 @@ roles_002: - :manage_documents - :view_wiki_pages - :edit_wiki_pages + - :protect_wiki_pages - :delete_wiki_pages - :add_messages - :manage_boards diff --git a/test/fixtures/wiki_pages.yml b/test/fixtures/wiki_pages.yml index f89832e44..ef7242430 100644 --- a/test/fixtures/wiki_pages.yml +++ b/test/fixtures/wiki_pages.yml @@ -4,19 +4,23 @@ wiki_pages_001: title: CookBook_documentation id: 1 wiki_id: 1 + protected: true wiki_pages_002: created_on: 2007-03-08 00:18:07 +01:00 title: Another_page id: 2 wiki_id: 1 + protected: false wiki_pages_003: created_on: 2007-03-08 00:18:07 +01:00 title: Start_page id: 3 wiki_id: 2 + protected: false wiki_pages_004: created_on: 2007-03-08 00:18:07 +01:00 title: Page_with_an_inline_image id: 4 wiki_id: 1 + protected: false \ No newline at end of file diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb index bf31e6614..8688c2e03 100644 --- a/test/functional/wiki_controller_test.rb +++ b/test/functional/wiki_controller_test.rb @@ -160,4 +160,60 @@ class WikiControllerTest < Test::Unit::TestCase get :index, :id => 999 assert_response 404 end + + def test_protect_page + page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page') + assert !page.protected? + @request.session[:user_id] = 2 + post :protect, :id => 1, :page => page.title, :protected => '1' + assert_redirected_to 'wiki/ecookbook/Another_page' + assert page.reload.protected? + end + + def test_unprotect_page + page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation') + assert page.protected? + @request.session[:user_id] = 2 + post :protect, :id => 1, :page => page.title, :protected => '0' + assert_redirected_to 'wiki/ecookbook' + assert !page.reload.protected? + end + + def test_show_page_with_edit_link + @request.session[:user_id] = 2 + get :index, :id => 1 + assert_response :success + assert_template 'show' + assert_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation/edit' } + end + + def test_show_page_without_edit_link + @request.session[:user_id] = 4 + get :index, :id => 1 + assert_response :success + assert_template 'show' + assert_no_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation/edit' } + end + + def test_edit_unprotected_page + # Non members can edit unprotected wiki pages + @request.session[:user_id] = 4 + get :edit, :id => 1, :page => 'Another_page' + assert_response :success + assert_template 'edit' + end + + def test_edit_protected_page_by_nonmember + # Non members can't edit protected wiki pages + @request.session[:user_id] = 4 + get :edit, :id => 1, :page => 'CookBook_documentation' + assert_response 403 + end + + def test_edit_protected_page_by_member + @request.session[:user_id] = 2 + get :edit, :id => 1, :page => 'CookBook_documentation' + assert_response :success + assert_template 'edit' + end end -- 2.39.5