##// END OF EJS Templates
Use \A and \z in validation regexps....
Jean-Philippe Lang -
r10734:0bd70d468051
parent child
Show More
@@ -1,251 +1,251
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'diff'
18 require 'diff'
19 require 'enumerator'
19 require 'enumerator'
20
20
21 class WikiPage < ActiveRecord::Base
21 class WikiPage < ActiveRecord::Base
22 include Redmine::SafeAttributes
22 include Redmine::SafeAttributes
23
23
24 belongs_to :wiki
24 belongs_to :wiki
25 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
25 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
26 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
26 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
27 acts_as_tree :dependent => :nullify, :order => 'title'
27 acts_as_tree :dependent => :nullify, :order => 'title'
28
28
29 acts_as_watchable
29 acts_as_watchable
30 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
30 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
31 :description => :text,
31 :description => :text,
32 :datetime => :created_on,
32 :datetime => :created_on,
33 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
33 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
34
34
35 acts_as_searchable :columns => ['title', "#{WikiContent.table_name}.text"],
35 acts_as_searchable :columns => ['title', "#{WikiContent.table_name}.text"],
36 :include => [{:wiki => :project}, :content],
36 :include => [{:wiki => :project}, :content],
37 :permission => :view_wiki_pages,
37 :permission => :view_wiki_pages,
38 :project_key => "#{Wiki.table_name}.project_id"
38 :project_key => "#{Wiki.table_name}.project_id"
39
39
40 attr_accessor :redirect_existing_links
40 attr_accessor :redirect_existing_links
41
41
42 validates_presence_of :title
42 validates_presence_of :title
43 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
43 validates_format_of :title, :with => /\A[^,\.\/\?\;\|\s]*\z/
44 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
44 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
45 validates_associated :content
45 validates_associated :content
46
46
47 validate :validate_parent_title
47 validate :validate_parent_title
48 before_destroy :remove_redirects
48 before_destroy :remove_redirects
49 before_save :handle_redirects
49 before_save :handle_redirects
50
50
51 # eager load information about last updates, without loading text
51 # eager load information about last updates, without loading text
52 scope :with_updated_on, lambda {
52 scope :with_updated_on, lambda {
53 select("#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on, #{WikiContent.table_name}.version").
53 select("#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on, #{WikiContent.table_name}.version").
54 joins("LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id")
54 joins("LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id")
55 }
55 }
56
56
57 # Wiki pages that are protected by default
57 # Wiki pages that are protected by default
58 DEFAULT_PROTECTED_PAGES = %w(sidebar)
58 DEFAULT_PROTECTED_PAGES = %w(sidebar)
59
59
60 safe_attributes 'parent_id', 'parent_title',
60 safe_attributes 'parent_id', 'parent_title',
61 :if => lambda {|page, user| page.new_record? || user.allowed_to?(:rename_wiki_pages, page.project)}
61 :if => lambda {|page, user| page.new_record? || user.allowed_to?(:rename_wiki_pages, page.project)}
62
62
63 def initialize(attributes=nil, *args)
63 def initialize(attributes=nil, *args)
64 super
64 super
65 if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
65 if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
66 self.protected = true
66 self.protected = true
67 end
67 end
68 end
68 end
69
69
70 def visible?(user=User.current)
70 def visible?(user=User.current)
71 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
71 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
72 end
72 end
73
73
74 def title=(value)
74 def title=(value)
75 value = Wiki.titleize(value)
75 value = Wiki.titleize(value)
76 @previous_title = read_attribute(:title) if @previous_title.blank?
76 @previous_title = read_attribute(:title) if @previous_title.blank?
77 write_attribute(:title, value)
77 write_attribute(:title, value)
78 end
78 end
79
79
80 def handle_redirects
80 def handle_redirects
81 self.title = Wiki.titleize(title)
81 self.title = Wiki.titleize(title)
82 # Manage redirects if the title has changed
82 # Manage redirects if the title has changed
83 if !@previous_title.blank? && (@previous_title != title) && !new_record?
83 if !@previous_title.blank? && (@previous_title != title) && !new_record?
84 # Update redirects that point to the old title
84 # Update redirects that point to the old title
85 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
85 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
86 r.redirects_to = title
86 r.redirects_to = title
87 r.title == r.redirects_to ? r.destroy : r.save
87 r.title == r.redirects_to ? r.destroy : r.save
88 end
88 end
89 # Remove redirects for the new title
89 # Remove redirects for the new title
90 wiki.redirects.find_all_by_title(title).each(&:destroy)
90 wiki.redirects.find_all_by_title(title).each(&:destroy)
91 # Create a redirect to the new title
91 # Create a redirect to the new title
92 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
92 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
93 @previous_title = nil
93 @previous_title = nil
94 end
94 end
95 end
95 end
96
96
97 def remove_redirects
97 def remove_redirects
98 # Remove redirects to this page
98 # Remove redirects to this page
99 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
99 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
100 end
100 end
101
101
102 def pretty_title
102 def pretty_title
103 WikiPage.pretty_title(title)
103 WikiPage.pretty_title(title)
104 end
104 end
105
105
106 def content_for_version(version=nil)
106 def content_for_version(version=nil)
107 result = content.versions.find_by_version(version.to_i) if version
107 result = content.versions.find_by_version(version.to_i) if version
108 result ||= content
108 result ||= content
109 result
109 result
110 end
110 end
111
111
112 def diff(version_to=nil, version_from=nil)
112 def diff(version_to=nil, version_from=nil)
113 version_to = version_to ? version_to.to_i : self.content.version
113 version_to = version_to ? version_to.to_i : self.content.version
114 content_to = content.versions.find_by_version(version_to)
114 content_to = content.versions.find_by_version(version_to)
115 content_from = version_from ? content.versions.find_by_version(version_from.to_i) : content_to.try(:previous)
115 content_from = version_from ? content.versions.find_by_version(version_from.to_i) : content_to.try(:previous)
116 return nil unless content_to && content_from
116 return nil unless content_to && content_from
117
117
118 if content_from.version > content_to.version
118 if content_from.version > content_to.version
119 content_to, content_from = content_from, content_to
119 content_to, content_from = content_from, content_to
120 end
120 end
121
121
122 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
122 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
123 end
123 end
124
124
125 def annotate(version=nil)
125 def annotate(version=nil)
126 version = version ? version.to_i : self.content.version
126 version = version ? version.to_i : self.content.version
127 c = content.versions.find_by_version(version)
127 c = content.versions.find_by_version(version)
128 c ? WikiAnnotate.new(c) : nil
128 c ? WikiAnnotate.new(c) : nil
129 end
129 end
130
130
131 def self.pretty_title(str)
131 def self.pretty_title(str)
132 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
132 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
133 end
133 end
134
134
135 def project
135 def project
136 wiki.project
136 wiki.project
137 end
137 end
138
138
139 def text
139 def text
140 content.text if content
140 content.text if content
141 end
141 end
142
142
143 def updated_on
143 def updated_on
144 unless @updated_on
144 unless @updated_on
145 if time = read_attribute(:updated_on)
145 if time = read_attribute(:updated_on)
146 # content updated_on was eager loaded with the page
146 # content updated_on was eager loaded with the page
147 begin
147 begin
148 @updated_on = (self.class.default_timezone == :utc ? Time.parse(time.to_s).utc : Time.parse(time.to_s).localtime)
148 @updated_on = (self.class.default_timezone == :utc ? Time.parse(time.to_s).utc : Time.parse(time.to_s).localtime)
149 rescue
149 rescue
150 end
150 end
151 else
151 else
152 @updated_on = content && content.updated_on
152 @updated_on = content && content.updated_on
153 end
153 end
154 end
154 end
155 @updated_on
155 @updated_on
156 end
156 end
157
157
158 # Returns true if usr is allowed to edit the page, otherwise false
158 # Returns true if usr is allowed to edit the page, otherwise false
159 def editable_by?(usr)
159 def editable_by?(usr)
160 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
160 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
161 end
161 end
162
162
163 def attachments_deletable?(usr=User.current)
163 def attachments_deletable?(usr=User.current)
164 editable_by?(usr) && super(usr)
164 editable_by?(usr) && super(usr)
165 end
165 end
166
166
167 def parent_title
167 def parent_title
168 @parent_title || (self.parent && self.parent.pretty_title)
168 @parent_title || (self.parent && self.parent.pretty_title)
169 end
169 end
170
170
171 def parent_title=(t)
171 def parent_title=(t)
172 @parent_title = t
172 @parent_title = t
173 parent_page = t.blank? ? nil : self.wiki.find_page(t)
173 parent_page = t.blank? ? nil : self.wiki.find_page(t)
174 self.parent = parent_page
174 self.parent = parent_page
175 end
175 end
176
176
177 # Saves the page and its content if text was changed
177 # Saves the page and its content if text was changed
178 def save_with_content
178 def save_with_content
179 ret = nil
179 ret = nil
180 transaction do
180 transaction do
181 if new_record?
181 if new_record?
182 # Rails automatically saves associated content
182 # Rails automatically saves associated content
183 ret = save
183 ret = save
184 else
184 else
185 ret = save && (content.text_changed? ? content.save : true)
185 ret = save && (content.text_changed? ? content.save : true)
186 end
186 end
187 raise ActiveRecord::Rollback unless ret
187 raise ActiveRecord::Rollback unless ret
188 end
188 end
189 ret
189 ret
190 end
190 end
191
191
192 protected
192 protected
193
193
194 def validate_parent_title
194 def validate_parent_title
195 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
195 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
196 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
196 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
197 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
197 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
198 end
198 end
199 end
199 end
200
200
201 class WikiDiff < Redmine::Helpers::Diff
201 class WikiDiff < Redmine::Helpers::Diff
202 attr_reader :content_to, :content_from
202 attr_reader :content_to, :content_from
203
203
204 def initialize(content_to, content_from)
204 def initialize(content_to, content_from)
205 @content_to = content_to
205 @content_to = content_to
206 @content_from = content_from
206 @content_from = content_from
207 super(content_to.text, content_from.text)
207 super(content_to.text, content_from.text)
208 end
208 end
209 end
209 end
210
210
211 class WikiAnnotate
211 class WikiAnnotate
212 attr_reader :lines, :content
212 attr_reader :lines, :content
213
213
214 def initialize(content)
214 def initialize(content)
215 @content = content
215 @content = content
216 current = content
216 current = content
217 current_lines = current.text.split(/\r?\n/)
217 current_lines = current.text.split(/\r?\n/)
218 @lines = current_lines.collect {|t| [nil, nil, t]}
218 @lines = current_lines.collect {|t| [nil, nil, t]}
219 positions = []
219 positions = []
220 current_lines.size.times {|i| positions << i}
220 current_lines.size.times {|i| positions << i}
221 while (current.previous)
221 while (current.previous)
222 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
222 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
223 d.each_slice(3) do |s|
223 d.each_slice(3) do |s|
224 sign, line = s[0], s[1]
224 sign, line = s[0], s[1]
225 if sign == '+' && positions[line] && positions[line] != -1
225 if sign == '+' && positions[line] && positions[line] != -1
226 if @lines[positions[line]][0].nil?
226 if @lines[positions[line]][0].nil?
227 @lines[positions[line]][0] = current.version
227 @lines[positions[line]][0] = current.version
228 @lines[positions[line]][1] = current.author
228 @lines[positions[line]][1] = current.author
229 end
229 end
230 end
230 end
231 end
231 end
232 d.each_slice(3) do |s|
232 d.each_slice(3) do |s|
233 sign, line = s[0], s[1]
233 sign, line = s[0], s[1]
234 if sign == '-'
234 if sign == '-'
235 positions.insert(line, -1)
235 positions.insert(line, -1)
236 else
236 else
237 positions[line] = nil
237 positions[line] = nil
238 end
238 end
239 end
239 end
240 positions.compact!
240 positions.compact!
241 # Stop if every line is annotated
241 # Stop if every line is annotated
242 break unless @lines.detect { |line| line[0].nil? }
242 break unless @lines.detect { |line| line[0].nil? }
243 current = current.previous
243 current = current.previous
244 end
244 end
245 @lines.each { |line|
245 @lines.each { |line|
246 line[0] ||= current.version
246 line[0] ||= current.version
247 # if the last known version is > 1 (eg. history was cleared), we don't know the author
247 # if the last known version is > 1 (eg. history was cleared), we don't know the author
248 line[1] ||= current.author if current.version == 1
248 line[1] ||= current.author if current.version == 1
249 }
249 }
250 end
250 end
251 end
251 end
General Comments 0
You need to be logged in to leave comments. Login now